Georeferencing API
    Preparing search index...

    Automatic plan matching

    @georeferencing/matching locates normalized image pixels inside a supplied reference snapshot. It runs locally in browser workers or Node worker threads. It ships algorithms, typed contracts, worker executors, reference acquisition and explicit controller-application helpers. It ships no UI components, React entry point, styles, or candidate-overlay renderer. Build the workflow your application needs with the framework and map renderer you already use.

    Install and choose an entry point

    pnpm add @georeferencing/core @georeferencing/matching
    # Only for a reference provider, the map library you already use:
    pnpm add ol@10 # /openlayers
    pnpm add leaflet@1 # /leaflet
    pnpm add maplibre-gl@6 # /maplibre

    Keep the core and matching versions aligned. Import the executor for your runtime; importing the shared root does not start workers or load OpenCV, React or a map library. No external processing service, telemetry or third-party CDN is required.

    API Purpose
    "@georeferencing/matching" Shared contracts, snapshots, WMS acquisition, numerical helpers and explicit application
    "@georeferencing/matching/browser" Browser worker factory and normalized-blob decoder
    "@georeferencing/matching/node" Node worker-thread factory
    "@georeferencing/matching/openlayers" Optional acquisition from configured WMS or loaded vector layers
    "@georeferencing/matching/leaflet" Optional acquisition from an L.tileLayer.wms layer
    "@georeferencing/matching/maplibre" Optional acquisition from a WMS-backed raster layer

    The API pages document individual option defaults, coordinate spaces, ownership, errors and disposal. The examples below are compiled against public package exports.

    Supply packed, EXIF-normalized RGBA pixels for the original query and an immutable reference snapshot. A request never applies control points automatically.

    import type { PixelImage, ReferenceSnapshot } from "@georeferencing/matching";
    import { createBrowserMatcher } from "@georeferencing/matching/browser";

    // Buffers use EXIF-normalized original pixel edges. Acquisition/decoding is host-owned.
    export async function matchInBrowser(
    query: PixelImage,
    reference: ReferenceSnapshot,
    signal?: AbortSignal,
    ) {
    const matcher = createBrowserMatcher();
    try {
    return await matcher.match({ query, reference }, { signal });
    } finally {
    matcher.dispose();
    }
    }

    This one-shot example disposes its worker in finally. For repeated searches, keep one matcher per application view and dispose it when the view is destroyed. Only one job may be active on an instance; abort an obsolete job before starting another. Successful requests reuse initialization and a bounded reference cache.

    Node callers choose their own decoder and normalize orientation before supplying pixels. No DOM, React, map renderer or native OpenCV installation is needed. Browser map rendering is outside Node parity; the pixel contract and matching semantics are shared.

    import type { PixelImage, ReferenceSnapshot } from "@georeferencing/matching";
    import { createNodeMatcher } from "@georeferencing/matching/node";

    export async function matchInNode(
    query: PixelImage,
    reference: ReferenceSnapshot,
    signal?: AbortSignal,
    ) {
    const matcher = createNodeMatcher();
    try {
    return await matcher.match(
    { query, reference, options: { maxMemoryBytes: 1024 ** 3 } },
    { signal },
    );
    } finally {
    matcher.dispose();
    }
    }

    The example explicitly reserves 1 GiB for a suitably provisioned host. Use a smaller query/reference or limits appropriate to your environment; raising the budget does not provision physical memory.

    @georeferencing/matching/node provides one function, createNodeMatcher(). It runs the same matching engine as the browser on a Node worker thread, so a long job does not block the event loop and an abort can stop it at any time. Results are identical to the browser's.

    Node has no image decoder or map renderer, so you supply both inputs as pixels:

    • The plan (query): packed RGBA from any decoder, with EXIF orientation already applied.
    • The search area (reference): pixels plus their georeferencing. Fetch them from a WMS server with createWmsProvider, which works in Node with the built-in fetch, or wrap a georeferenced image you already have with createSnapshot.

    Any decoder works. With sharp, for example:

    import type { PixelImage } from "@georeferencing/matching";
    import sharp from "sharp";

    async function decode(input: Blob | Uint8Array): Promise<PixelImage> {
    const bytes =
    input instanceof Blob ? new Uint8Array(await input.arrayBuffer()) : input;
    const { data, info } = await sharp(bytes)
    .rotate() // apply EXIF orientation
    .ensureAlpha()
    .raw()
    .toBuffer({ resolveWithObject: true });
    return { width: info.width, height: info.height, data: new Uint8Array(data) };
    }

    With a decoder in place, fetch the search area, match and read the result:

    import type {
    Extent,
    PixelImage,
    ReferenceSelection,
    XY,
    } from "@georeferencing/matching";
    import {
    createSnapshot,
    createWmsProvider,
    transform,
    } from "@georeferencing/matching";
    import { createNodeMatcher } from "@georeferencing/matching/node";

    /** Any decoder that returns packed RGBA with EXIF orientation applied. */
    export type Decode = (input: Blob | Uint8Array) => Promise<PixelImage>;

    // Fetch the search area from a WMS server, locate the plan in it and return the
    // plan's corners in map coordinates.
    export async function locatePlan(
    planFile: Uint8Array,
    selection: ReferenceSelection,
    decode: Decode,
    signal?: AbortSignal,
    ) {
    const query = await decode(planFile);
    const reference = await createWmsProvider({
    url: "https://example.org/wms",
    source: {
    id: "orthophoto",
    revision: "2026",
    layers: [...selection.layers],
    },
    decode,
    }).acquire(selection, signal);

    const matcher = createNodeMatcher();
    try {
    const result = await matcher.match({ query, reference }, { signal });
    const best = result.candidates[0];
    if (result.status !== "matched" || !best) return { status: result.status };

    // Plan pixel → reference pixel (candidate) → map coordinates (snapshot).
    const toMap = (p: XY) =>
    transform(reference.pixelToMap, transform(best.transform, p));
    const { width, height } = query;
    return {
    status: result.status,
    crs: reference.crs,
    corners: (
    [
    [0, 0],
    [width, 0],
    [width, height],
    [0, height],
    ] as XY[]
    ).map(toMap),
    };
    } finally {
    matcher.dispose();
    }
    }

    // Alternative reference: a north-up georeferenced image you already have, such as
    // a decoded orthophoto whose extent and CRS you know.
    export function orthophotoReference(
    pixels: PixelImage,
    extent: Extent,
    crs: string,
    ) {
    return createSnapshot({
    id: "orthophoto-2026",
    width: pixels.width,
    height: pixels.height,
    crs,
    extent,
    source: { id: "orthophoto", revision: "2026", layers: ["orthophoto"] },
    tiles: [{ ...pixels, x: 0, y: 0 }],
    });
    }

    candidate.transform maps plan pixels to reference pixels, and reference.pixelToMap maps reference pixels to map coordinates. Chaining both gives map coordinates for any plan pixel. To turn the match into control points instead, pass it to applyCandidate with a core GeoreferencerController, which also runs in Node. Typical uses are batch georeferencing on a server, preprocessing pipelines and automated checks.

    createWmsProvider accepts a host endpoint, request function and decoder. It preserves WMS layers/styles/filters/time, selected extent and actual resolution, assembles bounded tiles inside the selected area, and handles WMS 1.3 EPSG:4326 latitude/longitude order. Set axisOrder for other latitude-first CRSs. Noninteger extent/resolution ratios are rounded up in dimensions and the actual pixel-to-map mapping is recorded; there is no substitution of a different zoom level. Requests are sequential and cancellable. Service, CORS and loading failures are explicit. Credentials belong in host request closures, never source identity/provenance. Known credential parameter names are removed from snapshot metadata.

    Any provider can supply rendered WFS pixels. Node parity covers matching and the snapshot contract, not browser style rendering. Node can decode WMS responses using its own decoder.

    Map library Entry Built-in reference acquisition Everything else
    OpenLayers /openlayers One ImageWMS/TileWMS source, or selected loaded vector/WFS layers rendered in an isolated north-up map Host provider
    Leaflet /leaflet One L.tileLayer.wms layer Host provider
    MapLibre /maplibre One raster layer whose source tiles are WMS GetMap URLs with {bbox-epsg-3857} Host provider; vector tiles are not captured

    Every WMS path requests fresh images of exactly the selected area from the server; none reads the rendered map, so the layer may be hidden. openLayersSelection, leafletSelection and mapLibreSelection turn the current view into a selection. Leaflet selections use the map's CRS; MapLibre selections are EPSG:3857 and require zero pitch. The Leaflet and MapLibre entries do not import their library at runtime.

    import {
    createLeafletProvider,
    leafletSelection,
    } from "@georeferencing/matching/leaflet";
    import {
    createMapLibreProvider,
    mapLibreSelection,
    } from "@georeferencing/matching/maplibre";
    import {
    createOpenLayersProvider,
    openLayersSelection,
    } from "@georeferencing/matching/openlayers";
    import type * as L from "leaflet";
    import type { Map as MapLibreMap } from "maplibre-gl";
    import type BaseLayer from "ol/layer/Base.js";
    import type OLMap from "ol/Map.js";

    // Each provider reads the WMS configuration of a layer the application already
    // shows; each selection helper turns the current view into the search area.

    export function openLayersReference(map: OLMap, wmsLayer: BaseLayer) {
    const provider = createOpenLayersProvider([
    { id: "ortho", layer: wmsLayer, revision: "2026" },
    ]);
    return provider.acquire(openLayersSelection(map, ["ortho"]));
    }

    export function leafletReference(map: L.Map, wmsLayer: L.TileLayer.WMS) {
    const provider = createLeafletProvider([
    { id: "ortho", layer: wmsLayer, revision: "2026" },
    ]);
    return provider.acquire(leafletSelection(map, ["ortho"]));
    }

    // `layer` is the style layer ID of a raster layer whose source tiles are WMS
    // GetMap URLs with `{bbox-epsg-3857}`.
    export function mapLibreReference(map: MapLibreMap, layer: string) {
    const provider = createMapLibreProvider([
    { id: "ortho", map, layer, revision: "2026" },
    ]);
    return provider.acquire(mapLibreSelection(map, ["ortho"]));
    }

    OpenLayers vector capture copies currently loaded features and style configuration; wait for host WFS loading before acquisition. Style callbacks must be deterministic and independent of mutable host state. No feature loader is triggered on the copy. Vector capture requires an extent aligned to the resolution grid, is capped at 8 MP/8192 pixels per side, uses pixel ratio 1, and excludes editor overlays. Unknown raster renderers and mixed WMS/vector compositions require a host provider.

    For a browser WMS integration, inject transport and decode functions. Acquisition runs before matching, so measure its duration separately from algorithm diagnostics.

    import type { ReferenceSelection } from "@georeferencing/matching";
    import { createWmsProvider } from "@georeferencing/matching";
    import { decodeReferenceImage } from "@georeferencing/matching/browser";

    // Browser example: the host owns authentication and the eligible WMS endpoint.
    export async function acquirePlanReference(
    endpoint: string,
    selection: ReferenceSelection,
    sourceRevision: string,
    signal?: AbortSignal,
    ) {
    const provider = createWmsProvider({
    url: endpoint,
    source: {
    id: "engineering-plans", // Opaque identity; never a credential-bearing URL.
    revision: sourceRevision,
    layers: [...selection.layers],
    },
    version: "1.3.0",
    maxPixels: 8_000_000,
    maxTileSize: 1024,
    decode: decodeReferenceImage,
    request: async (url, requestSignal) => {
    const response = await fetch(url, {
    signal: requestSignal,
    credentials: "same-origin",
    });
    if (!response.ok) throw new Error(`Reference HTTP ${response.status}`);
    return response.blob();
    },
    });
    return provider.acquire(selection, signal);
    }

    For caller-rendered WFS or another raster source, implement "@georeferencing/matching".ReferenceProvider and create its result with "@georeferencing/matching".createSnapshot. The provider must preserve the selected extent, CRS, exact pixel-to-map mapping, source/style revision, and missing source data. Capture selected reference content without UI or candidate overlays. A snapshot copies bytes and freezes metadata; it does not keep live map references.

    All public pixel coordinates use normalized full-image edges: top-left (0,0), Y down, first centre (0.5,0.5). OpenCV centres, crops and resizing are converted explicitly. queryRegion is an optional rectangle in that same coordinate space. It defines the plan footprint; feature/colour exclusion never changes that region. Transparent pixels are composited over white and excluded from evidence. White paper remains valid data. technical-plan uses separate min/max contrast normalization; generic retains grayscale contrast. Optional suppressColor excludes strongly coloured features; leave it disabled when colour carries detail.

    Buffers are packed RGBA without row padding. Snapshot creation clones pixels and metadata, and executors structured-clone requests without detaching caller buffers. Do not mutate snapshot byte views. Snapshot metadata is frozen; map navigation does not change it. All tiles partition the same raster with integer offsets; explicit valid: false tiles or alpha-zero pixels represent missing data. Internal tile edges do not define coverage. Extraction windows read across acquisition seams.

    pixelToMap is a row-major 3×3 matrix applied to column vectors. The default is an exact north-up edge mapping from extent; supply the exact matrix for rotated rasters. Nonlinear host mappings stay outside worker messages: provide a pixelToMap callback to applyCandidate and a matching host overlay renderer. No pixel polygon is advertised as longitude/latitude GeoJSON. CRS conversion and full-image domain validation on application use the existing core registry/fitter.

    options.detector selects how features are found and compared. SIFT is the default; AKAZE is used only when a request sets detector: "akaze". Both detectors ship in the same WASM file, run the same search and the same acceptance checks, and are covered by the same tests. The detector changes how much evidence is found and how fast, never what counts as a valid match.

    SIFT (default) AKAZE
    Descriptor 128 floats, L2 distance 486 bits, Hamming distance
    Benchmark plans, first match 1.1–1.9 s 0.3–1.0 s
    Same reference again (cached features) about 0.7 s about 0.45 s
    Small plan in a 5000×5000 reference about 17 s about 11 s
    Peak WASM heap about 240 MiB about 96 MiB
    Benchmark plans matched 8 of 8 6 of 8
    Corner error when matched 0.05–1.2 px 0.02–1.0 px

    Times are single-worker Node measurements on an Apple M2 from the repository's matching benchmark; compare the ratios rather than the absolute values. The up-front memory budget check is the same for both detectors.

    Use SIFT when one reliable attempt matters more than speed, and especially when:

    • the plan's scale differs noticeably from the reference, for example a scan at a lower resolution than the reference tiles;
    • annotations, stamps, coloured markup or handwriting cover the linework;
    • the plan only partly overlaps the search area, or a small plan is searched in a large area. More matched points leave more margin over the acceptance checks.

    Use AKAZE when speed or memory matters and the input is favourable:

    • interactive re-runs while a user adjusts the search area, batch jobs over many plans, or memory-constrained browsers;
    • clean plans at roughly the reference's scale, such as exports from the same CAD or GIS source, or scans resampled to the reference resolution;
    • when you can retry with SIFT after a not-found.

    AKAZE finds fewer matching points on hard input. When that is too few, it returns not-found rather than a different placement: in the benchmark it misses the plan scaled to 65% and the warped, heavily annotated plan, which SIFT both matches. A fallback keeps most of AKAZE's speed:

    let result = await matcher.match({ ...request, options: { detector: "akaze" } });
    if (result.status === "not-found")
    result = await matcher.match({ ...request, options: { detector: "sift" } });

    A matcher caches the reference features of its last request only, keyed by detector and options. Alternating detectors on one matcher therefore extracts the reference again on every switch; keep one matcher per detector if you alternate repeatedly.

    Candidates are ordered by independent support, its spatial distribution, distinctiveness, residuals, split-fit stability and structural agreement. Score version evidence/1 is a ranking measure, not a probability. A stronger partial candidate can outrank a weaker complete one. Repeated reference copies survive candidate generation; duplicate transforms are merged. Repetitive query glyphs, collinearity, poles, mirrored or collapsed footprints, weak and unstable fits are rejected. At least 12 independent correspondences are required; this alone is insufficient.

    • matched: the top validated candidate has a sufficient score gap.
    • ambiguous: validated alternatives have similar scores; inspect each one.
    • not-found: insufficient evidence, not proof that the plan is absent.

    overlapFraction is the area of the selected query region inside the search area, computed by inverse mapping the clipped footprint. referenceDataCoverage separately measures available source pixels as a fraction of the full query region (alpha coverage uses a deterministic grid). supportCoverage is the correspondence hull divided by observable overlap. These three quantities are not interchangeable. extentStatus uses a one-reference-pixel boundary tolerance; the unrounded overlap fraction is still returned. Hosts should distinguish extrapolation beyond the observed overlap when rendering footprints. Residuals are in original reference pixels, not survey accuracy.

    Use the headless APIs with your preferred UI framework. The package does not provide a React entry point or stylesheet. Reference acquisition is separate from matching; the optional map entries capture pixels and do not draw results.

    1. Capture a createApplicationToken(controller, configurationRevision) before acquisition. The host revision must cover source data/styles, selected layers, extent, resolution, query region and matching configuration.
    2. Acquire a ReferenceSnapshot with a provider. Decode the query from controller.getNormalizedImage(signal) with decodeReferenceImage in a browser, or supply normalized RGBA from your own decoder.
    3. Call matcher.match, pass an abort signal and render its progress in your UI. Cancel and invalidate results when the image, alignment or host configuration changes. Keep an application job/generation ID to discard late acquisition work.
    4. Present ranked candidates and their coverage/uncertainty. footprint and overlap are reference pixels, so map them with transform(snapshot.pixelToMap, point) and your map projection. For nonlinear mappings use densifyBoundary. Style observed overlap separately from extrapolated footprint in your own map.
    5. Only on explicit user acceptance, call applyCandidate with the original result, selected candidate object, snapshot/token and current configuration revision. The default merges manual points and keeps the document's fit model; offer replacement, or model: "candidate" to adopt the matched model, only by explicit choice.

    Application is one undoable controller edit, with up to 16 distributed proposed points by default, explicit CRS/source provenance and the core control-point cap. It fits/validates the full image before mutation, and rejects stale results. Continue through the existing alignment review and export/save flow after adding points.

    The private demo's MatchingPanel and overlay renderer are examples for hosts to adapt, not library exports. The React editor's optional matchingPanel slot accepts host content; it does not import matching. Run pnpm dev, choose OpenLayers, then Find points automatically and Load example image. The existing four steps remain unchanged. The demo uses project-owned generated marks; use your actual reference plan layers in your app.

    The following browser example separates preparing a result from accepting it. Keep the original token, snapshot and result together while the user reviews them.

    import type { GeoreferencerController } from "@georeferencing/core";
    import type {
    ApplicationToken,
    ImageMatcher,
    MatchExecution,
    MatchResult,
    ReferenceProvider,
    ReferenceSelection,
    ReferenceSnapshot,
    } from "@georeferencing/matching";
    import {
    applyCandidate,
    createApplicationToken,
    } from "@georeferencing/matching";
    import { decodeReferenceImage } from "@georeferencing/matching/browser";

    interface CandidateReview {
    token: ApplicationToken;
    snapshot: ReferenceSnapshot;
    result: MatchResult;
    }

    // Called by the host's Start action. This never changes control points.
    export async function prepareCandidateReview(
    controller: GeoreferencerController,
    matcher: ImageMatcher,
    provider: ReferenceProvider,
    selection: ReferenceSelection,
    configurationRevision: string,
    execution: MatchExecution = {},
    ): Promise<CandidateReview> {
    const token = createApplicationToken(controller, configurationRevision);
    const snapshot = await provider.acquire(
    structuredClone(selection),
    execution.signal,
    );
    execution.signal?.throwIfAborted();
    const query = await decodeReferenceImage(
    await controller.getNormalizedImage(execution.signal),
    );
    execution.signal?.throwIfAborted();
    const result = await matcher.match({ query, reference: snapshot }, execution);
    return { token, snapshot, result };
    }

    // Called separately, only after explicit selection and acceptance in the host UI.
    export function applyReviewedLocation(
    controller: GeoreferencerController,
    review: CandidateReview,
    selectedId: string,
    currentConfigurationRevision: string,
    mode: "merge" | "replace" = "merge",
    ) {
    const candidate = review.result.candidates.find((c) => c.id === selectedId);
    if (!candidate)
    throw new Error("Select a candidate from the current review.");
    applyCandidate(
    controller,
    review.result,
    candidate,
    review.snapshot,
    review.token,
    { configurationRevision: currentConfigurationRevision, mode },
    );
    }

    The surrounding application owns these lifecycle rules:

    • Show acquisition progress before algorithm stages begin. MatchProgress.total describes the current stage; zero means indeterminate, not completed.
    • Pass the same AbortSignal to acquisition and matching. Invalidate review state and increment a host job ID whenever selection, image, alignment or source configuration changes. Ignore late results from a previous job even if a custom provider fails to stop immediately after cancellation.
    • Show matched, ambiguous and not-found distinctly. Let the user inspect each candidate, observed overlap, unavailable source data and extrapolation. A score is a ranking measure, not a correctness probability.
    • Invoke applyReviewedLocation only from an explicit acceptance action with the current configuration revision. Do not create a fresh token at acceptance time. The revision guard rejects edits made while results were being reviewed.
    • Abort outstanding work, clear owned overlays and call matcher.dispose() on unmount/shutdown. Starting matching and displaying results do not alter points.

    Default limits: 25 MP query, 32 MP reference, 1 GiB reservation (enough for both at once), 24,000 features per image, 1,600 features per window, 1,024-pixel coarse edge and processing tiles with 96-pixel internal halos, five candidates, one active job. Dimensions are capped at 32,768 per side; references at 4,096 acquisition tiles. The WASM heap has a hard 512 MiB maximum; live image matrices and descriptors are deleted after each unit. One content-checked reference feature cache is retained, and reset on disposal. The estimate includes input copies, descriptor storage and the capped WASM heap; it is not a guarantee about browser/GPU/host decoder memory.

    A 5,000×5,000 query was exercised on a 16 GiB Apple M2 with an explicit 1 GiB matching reservation. Use MATCHING_CORE_LIMITS when creating the existing core worker engine (25 MP input, 64 MiB encoded bytes, 1 GiB memory). Matching consumes decoded pixels; the caller enforces its encoded-byte limit before decoding. Core's default 24 MP output limit remains separate; increase only when the intended export fits the host's output memory budget. The original manual defaults are unchanged.

    Abort terminates the worker even during synchronous WASM and makes late messages irrelevant. The next request creates a fresh worker. Disposal is idempotent; requests afterward reject DISPOSED. Concurrent requests reject BUSY. Errors use INPUT, BUDGET, BACKEND, SOURCE, STALE, or GEOMETRY; cancellation is an AbortError. Worker messages carry a job ID and serialized progress/result/error; callbacks, signals, DOM nodes and map objects are never posted.

    The shipped artifact is trimmed OpenCV 4.12.0, built with Emscripten 6.0.8, single threaded scalar WASM. Runtime startup checks SIFT, AKAZE, BFMatcher, findHomography and perspectiveTransform. SIFT is the default; AKAZE is a supported alternative with the trade-offs described under "Choose a detector". Detection is isolated in features.ts; executor/result contracts do not depend on a detector. The local artifact is about 4.4 MB uncompressed; see dist/licenses/opencv-provenance.json for exact hashes and flags. Build from source with bash scripts/matching/build-opencv.sh in the repository. Required Apache, BSD and third-party notices accompany the assets.

    Repository validation: pnpm test:matching, pnpm benchmark:matching (requires private local plans), pnpm test:browser, pnpm test:consumer, pnpm release:check. Synthetic fixtures are deterministic MIT-licensed source. Private image files and derived browser fixtures stay ignored under plans/ and artifacts/ and never ship. Matching independently drawn styles is not universally reliable: preserve manual review, and never interpret these fixture results as positional-accuracy guarantees.

    "@georeferencing/matching".MatchingError has a stable code. Decoder, custom-provider and worker-creation errors can also propagate their native type.

    Code / error Host response
    INPUT Correct image buffers, query region, selection or options before retrying
    BUDGET Reduce query/reference size or configure a measured suitable memory budget; application also needs free GCP slots
    SOURCE Fix reference loading, credentials, CORS, rendering or decoder failures; do not report these as not-found
    BACKEND Check packaged worker/WASM URLs, CSP and artifact compatibility
    BUSY Wait for or abort the current job before reusing the matcher
    DISPOSED Create a new matcher for a new owning view/session
    STALE Discard the old review and capture/match current inputs again
    GEOMETRY Reject the invalid mapping; preserve manual work
    AbortError Clear busy state for the cancelled job; do not display it as an algorithm failure

    With Vite, the default browser module worker and local WASM are emitted as assets. Other bundlers can supply BrowserMatcherOptions.workerFactory and wasmUrl. The factory must create a new dedicated worker after an abort; the matcher owns and terminates that worker. Never lend it a shared application worker.

    Exports /worker, /opencv.js and /opencv.wasm support host-controlled copying. When copying directly, retain the worker's vendor/opencv.js and vendor/opencv.wasm relative paths. Serve WASM as application/wasm; use same-origin assets and CSP script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'. Neither SIMD nor cross-origin isolation is required by this build. Node resolves assets beside the installed package; a wasmUrl override is an absolute local filesystem path. Retain the bundled licenses/notices when redistributing assets.

    See workers and deployment for shared core worker configuration and the matching API for entry points.