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.
-- 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);
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:
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
-- 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:
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
# 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:
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.
-- 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;
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.
Related
- Scoping Rules for Spatial Products — the parent topic and the scope-change rules
- Defining Scoping Rules for Enterprise Spatial Products — the per-manifest gates this sits above
- Splitting an Overloaded Spatial Domain — where two successors must not both claim a region