Checking Registry-Wide Extent Overlap

Schema validation and containment policy both look at one manifest, and both will happily approve an extent that already belongs to another domain. Because the extent is what resolves ownership, two products claiming the same region make routing non-deterministic — which domain answers depends on evaluation order, and the answer can change between deployments with nothing in either manifest having changed. This guide adds the gate that only a registry-wide check can provide: index every declared extent, test a candidate against all of them, and reject an intersection with the conflicting product named. It closes the gap identified in Scoping Rules for Spatial Products within Geospatial Data Mesh Fundamentals, and it is the precondition for the widening described in Defining Scoping Rules for Enterprise Spatial Products.

Prerequisites

Requirement Value / Assumption Notes
Tools PostGIS ≥ 3.3, python3 ≥ 3.11, the registry API The registry needs a spatial index of its own
CRS convention Every declared extent stored in EPSG:4326 Comparing across SRIDs returns nothing
Registry One row per published product with its coverage geometry Not the bounding box — the coverage
Access roles Registry service applies the gate; nobody bypasses it An advisory overlap check is not a gate
Environment REGISTRY_DSN, CANDIDATE_MANIFEST Exported before running

Step-by-Step Implementation

1. Index the registry’s declared extents

An overlap check that scans every product linearly becomes the slowest part of registration once an estate has a few hundred products.

sql
-- registry_schema.sql — declared coverage, spatially indexed.
CREATE TABLE IF NOT EXISTS product_coverage (
    product_id   TEXT PRIMARY KEY,
    domain       TEXT NOT NULL,
    layer        TEXT NOT NULL,
    coverage     geometry(MultiPolygon, 4326) NOT NULL,
    declared_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- The whole point of the index: candidate checks become sub-linear.
CREATE INDEX IF NOT EXISTS product_coverage_gix
    ON product_coverage USING GIST (coverage);

-- Two products in the same layer must not overlap; different layers legitimately may.
CREATE INDEX IF NOT EXISTS product_coverage_layer_idx
    ON product_coverage (layer);

What counts as a conflict, and what legitimately co-locatesFour pairings against whether they conflict and why. Two parcels products covering one city conflict, because both claim to be the authoritative source for the same thing in the same place. A parcels product and a flood-extents product covering the same city do not, because they answer different questions and the routing plane resolves by layer as well as extent. Two versions of one product do not conflict, because version resolution precedes ownership resolution. Two adjacent parcels products sharing a boundary do not.ConflictsBecausetwo parcels products, one cityyesboth claim authorityparcels + flood extentsnodifferent layerstwo versions of one productnoversion resolves firstadjacent parcels productsnozero-area intersection

The layer distinction is load-bearing. A parcels product and a flood-extents product covering the same city do not conflict — they answer different questions. Two parcels products covering it do.

Verify the index is used rather than merely present:

bash
psql "$REGISTRY_DSN" -c "EXPLAIN ANALYZE
  SELECT product_id FROM product_coverage
  WHERE layer = 'parcels'
    AND coverage && ST_GeomFromText('POLYGON((-1 50,1 50,1 52,-1 52,-1 50))', 4326);" \
  | grep -E 'Index Scan|Seq Scan'

2. Test the candidate against every registered product in its layer

sql
-- overlap_check.sql — intersections between a candidate and the registry.
-- Parameters: :candidate_wkt (text), :layer (text), :self_id (text)
WITH candidate AS (
    SELECT ST_GeomFromText(:'candidate_wkt', 4326) AS g
)
SELECT
    p.product_id,
    p.domain,
    ST_Area(ST_Intersection(p.coverage, c.g)::geography) AS overlap_m2,
    -- Share of the candidate that is already claimed, which distinguishes a
    -- boundary-precision artefact from a genuine double claim.
    ST_Area(ST_Intersection(p.coverage, c.g)::geography)
        / nullif(ST_Area(c.g::geography), 0)           AS overlap_fraction
FROM product_coverage p, candidate c
WHERE p.layer = :'layer'
  AND p.product_id <> :'self_id'      -- re-registering a product is not self-conflict
  AND p.coverage && c.g               -- index-backed prefilter
  AND ST_Intersects(p.coverage, c.g)  -- exact, on the survivors
ORDER BY overlap_m2 DESC;

Verify the check distinguishes a shared boundary from a real overlap — adjacent extents touch and do not conflict:

The gate that needs the whole registry, and where it sitsA candidate manifest passes three per-manifest checks — structural schema, containment against the domain perimeter, and jurisdictional policy — each of which needs only the manifest and some policy data. The overlap check sits above them and needs every other registered product in the same layer, which is why it cannot be evaluated earlier and why it is the layer most often missing. Its rail leads to a rejection naming the conflicting product and its owning domain, so the submitter knows who to talk to.Schemamanifest aloneContainment+ the domainJurisdiction+ policy dataRegistry overlap+ every productRegisteredrouting deterministicvalidcontainedlawfuluniqueconflicting product namedRejected, not warnedan advisory check is not a gate

bash
psql "$REGISTRY_DSN" --csv -f overlap_check.sql \
  -v candidate_wkt="$(jq -r '.coverage_wkt' "$CANDIDATE_MANIFEST")" \
  -v layer=parcels -v self_id=none
# ST_Intersects is true for a shared edge; the area of that intersection is zero,
# which is why the fraction column and not the boolean decides.

3. Reject with the conflict named, and a tolerance for touching edges

python
# overlap_gate.py — the registration gate.
TOUCH_TOLERANCE = 1e-9      # fraction of the candidate; a shared edge, not an overlap


class OverlapRejected(Exception):
    pass


def gate(conflicts: list[dict]) -> None:
    """A shared boundary produces a zero-area intersection and is fine. Anything
    with real area is two products claiming one region, which makes routing
    depend on evaluation order rather than on ownership."""
    real = [c for c in conflicts if float(c["overlap_fraction"]) > TOUCH_TOLERANCE]
    if not real:
        return
    lines = [
        f"  {c['product_id']} (domain {c['domain']}): "
        f"{float(c['overlap_fraction']) * 100:.2f}% of the candidate, "
        f"{float(c['overlap_m2']) / 1e6:.1f} km²"
        for c in real
    ]
    raise OverlapRejected(
        "declared extent overlaps registered product(s):\n" + "\n".join(lines)
        + "\n\nResolve with the owning domain(s) before re-submitting."
    )

Verify the rejection names the counterpart — a rejection that says only “overlap detected” produces a support ticket rather than a conversation:

Distinguishing a shared boundary from a real double claimFour intersection cases expressed as a fraction of the candidate area, on a scale where the touch tolerance sits at ten to the minus nine. Two adjacent extents sharing an edge produce an intersection around ten to the minus fifteen, purely floating-point noise. A one-metre coordinate rounding overlap sits near ten to the minus eleven. A genuine city-block overlap is around 0.004 of the candidate, and a whole-region double claim is 0.62. The gap between the noise cases and the real ones spans several orders of magnitude, which is what makes the tolerance easy to set.shared edge1e-15floating-point noise1 m rounding1e-11city block overlap0.004a real double claimregion double claim0.62touch tolerance

bash
python3 overlap_gate.py --manifest "$CANDIDATE_MANIFEST" 2>&1 | head -6

4. Re-run the check across the whole registry on a schedule

Registrations are gated, and coverage can still drift — a product republished with a recomputed coverage, a manual correction, a migration.

sql
-- registry_sweep.sql — every overlapping pair currently in the registry.
SELECT a.product_id AS product_a, b.product_id AS product_b, a.layer,
       ST_Area(ST_Intersection(a.coverage, b.coverage)::geography) / 1e6 AS overlap_km2
FROM product_coverage a
JOIN product_coverage b
  ON a.layer = b.layer
 AND a.product_id < b.product_id      -- each pair once
 AND a.coverage && b.coverage
 AND ST_Intersects(a.coverage, b.coverage)
WHERE ST_Area(ST_Intersection(a.coverage, b.coverage)::geography) > 1.0
ORDER BY overlap_km2 DESC;
bash
psql "$REGISTRY_DSN" --csv -f registry_sweep.sql | tee /tmp/sweep.csv
[ "$(wc -l < /tmp/sweep.csv)" -eq 1 ] && echo "registry is conflict-free"

Configuration Reference

Parameter Value Effect
Coverage geometry The declared polygon, not a bbox A bbox over-claims concave coverage
Comparison CRS EPSG:4326 Mixing SRIDs returns no intersections
Scope Same layer only Different layers legitimately co-locate
TOUCH_TOLERANCE 1e-9 of candidate area Adjacent extents share an edge
Self-exclusion By product_id Re-registration is not self-conflict
Sweep cadence Daily Catches drift that registration gating cannot
Verdict Rejection An advisory overlap check is not a gate

Common Failure Modes & Fixes

Adjacent products are rejected for touching. Root cause: testing ST_Intersects alone, which is true for a shared edge. Fix: decide on intersection area against a tolerance, as above.

The check passes and routing is still ambiguous. Root cause: the registry holds bounding boxes rather than coverage polygons, so two concave coverages that do not really overlap have overlapping envelopes — or two that do overlap have envelopes that suggest they do not. Fix: store the coverage geometry.

Registration becomes slow as the estate grows. Root cause: no GiST index, so every candidate is compared against every product. Fix: the index above; the prefilter is what makes the check sub-linear.

A product overlaps itself after a republish. Root cause: the self-exclusion compares on a name that changed, so the previous registration looks like a different product. Fix: exclude on a stable product identifier, never on a version or a display name.

The sweep finds overlaps the gate approved. Root cause: coverage was recomputed on republish and grew — often the reprojected-envelope creep where each cycle derives the extent from the previous one. Fix: recompute coverage from source geometry, and treat the sweep as the backstop it is.

FAQ

Why is overlap a rejection rather than a warning?

Because the consequence is non-deterministic routing, which is the hardest class of fault to diagnose. With two products claiming a region, which one answers depends on the routing plane’s evaluation order — so a request can be served by one domain today and the other after an unrelated deployment, with no change in either manifest and nothing in any log to explain it. Consumers see intermittently different data from the same query. A warning at registration converts that into a permanent condition nobody owns; a rejection forces the two domains to agree while the change is still cheap.

Should different layers ever be checked against each other?

No, and doing so would make the estate unworkable. A parcels product and a flood-extents product covering the same city are answering different questions and must both claim it; the routing plane resolves by layer as well as by extent. The conflict that matters is two products claiming to be the authoritative source for the same thing in the same place, which is exactly what a same-layer overlap means.

What tolerance is right for touching edges?

Small enough that a genuine overlap cannot hide in it, and large enough to absorb coordinate rounding. A fraction of the candidate’s area around 1e-9 works because a shared boundary produces a mathematically zero-area intersection and floating-point evaluation gives something on the order of 1e-15 — many orders of magnitude below any real double claim. Expressing the tolerance as a fraction rather than an absolute area keeps it meaningful across products differing in size by orders of magnitude.

How does this interact with a deliberate widening?

Widening is exactly the operation this gate exists to check. Extending a product’s extent looks additive from the product’s own perspective and can move requests that a neighbouring domain was serving, so the widening is only safe once it has been tested against every other declared extent in its layer. That makes the check a precondition of widening rather than merely of first registration, and it is why the gate runs on every submission rather than only on new products.