Security Boundaries in Spatial QA

A security boundary in geospatial testing is a deterministic, enforceable line that validation code is not permitted to cross: a tenant whose geometry must never join another tenant’s, a coordinate that must never round outside its sensitivity tier, or a memory ceiling a topology check must never breach. As a strategy within Geospatial QA Fundamentals & Architecture, boundary enforcement reframes security as something the test suite asserts on every commit rather than something an auditor inspects after the fact. This matters in spatial pipelines because the failure modes are silent: a botched coordinate reference system (CRS) transform or an over-permissive bounding box does not throw — it produces plausible, wrong, and sometimes confidential output that flows straight into tile servers and analytical warehouses. The patterns here turn isolation, tolerance, and access control into executable predicates that run inside the same gates as your geometry and schema checks.

Classifying a spatial QA security boundary into four enforceable families A root node, "Spatial QA security boundary", splits into four columns. Column one, isolation and tenancy, enforces no cross-tenant join with the predicate ST_Disjoint. Column two, precision and drift, keeps output within the sensitivity grid with equals_exact within tolerance. Column three, resource and memory, prevents heap exhaustion with a bounded batch cap. Column four, access and redaction, keeps sensitive coordinates from a shared sink with snap-to-grid. Each family box feeds a predicate box below it. Spatial QA security boundary Isolation / tenancy no cross-tenant join Precision / drift stay within grid tier Resource / memory bound the heap Access / redaction keep coords off sink ST_Disjoint hard fail · zero overlap equals_exact(tol) symmetric epsilon bounded batch cap R-tree pre-filter snap-to-grid coarsen + null mask

A Taxonomy of Spatial Security Boundaries

Spatial boundaries fall into four enforceable families, and each maps to a distinct predicate, tolerance strategy, and failure signature. Treating them as one undifferentiated “security” concern is the most common reason boundary checks are too loose to catch real leaks. Before writing any geometric assertion you should pin the spatial tolerance thresholds for the family in play, because a tenant-isolation check and a precision-drift check tolerate error in opposite directions.

Boundary family What it enforces Core predicate Tolerance strategy Threshold range CRS units
Isolation / tenancy No cross-tenant or cross-classification spatial join ST_Disjoint, ST_Intersects Hard fail, zero overlap Exact (no slack) n/a (relation)
Precision / drift Output geometry stays within sensitivity-tier grid equals_exact(g, tol) Symmetric epsilon 1e-61e-9 deg; 1e-3 m degrees / metres
Resource / memory Validation cannot exhaust the runner heap bounded batch size Hard cap per batch 5k–50k features n/a
Access / redaction Sensitive coordinates never reach a shared sink snap-to-grid, attribute mask Coarsen + null grid ≥ 100 m metres

The threshold ranges are deliberately CRS-aware. A 1e-6 epsilon means roughly 0.11 m at the equator in WGS84 (EPSG:4326) but is meaningless in a projected metre-based system such as EPSG:25832, where you instead express tolerance directly in millimetres. Mixing the two is itself a boundary violation — see the gotchas below.

Isolation Boundaries: Tenant and Classification Separation

Isolation boundaries guarantee that geometries from different tenants, security classifications, or licensing tiers are never silently combined. The predicate is relational rather than metric: adjacent administrative zones owned by different tenants must satisfy ST_Disjoint, while a feature claimed by tenant A must ST_Contains only A’s geometries. In Shapely 2.x the vectorised form reads shapely.disjoint(tenant_a, tenant_b), and the assertion fails closed — any returned False is a leak, not a warning. Because spatial joins are the usual leak vector, scope every join through the dataset’s sensitivity tier as defined by your scoping rules for map data validation, so a high-throughput batch can never widen its extent past the tier it was granted.

import shapely

# Isolation assertion: no geometry may straddle two tenant envelopes.
def assert_tenant_isolation(geom, own_envelope, foreign_envelope) -> None:
    assert shapely.contains(own_envelope, geom), "geometry escaped its own tenant scope"
    assert shapely.disjoint(geom, foreign_envelope), "geometry intersects a foreign tenant"

Precision Boundaries: Tolerance and Coordinate Drift

Precision boundaries cap how far a transformed or round-tripped geometry may move from its source. The risk is twofold: too tight a tolerance floods CI with false positives from ordinary floating-point noise, while too loose a tolerance lets a real CRS or datum error pass as “close enough.” The correct test is a symmetric relative-error bound. For two geometries AA and BB the boundary holds when the Hausdorff distance stays under a CRS-scaled epsilon:

dH(A,B)=max{supaAinfbBab,  supbBinfaAab}εd_H(A, B) = \max\left\{\sup_{a \in A} \inf_{b \in B} \lVert a - b \rVert,\; \sup_{b \in B} \inf_{a \in A} \lVert a - b \rVert\right\} \le \varepsilon

In Shapely 2.x use a.equals_exact(b, tolerance=eps) for vertex-level equality within eps, or shapely.hausdorff_distance(a, b) for the envelope above. Load eps from config keyed on CRS units — never hardcode it — and pin it to the source data’s acquisition accuracy.

import shapely

def assert_within_tolerance(source, transformed, eps: float) -> None:
    # eps is in the CRS unit of `transformed` (degrees for 4326, metres for projected).
    assert shapely.hausdorff_distance(source, transformed) <= eps, "geometry drifted past tolerance"

Resource Boundaries: Memory-Safe Validation

Resource boundaries stop a validation run from becoming a denial-of-service against your own CI. Operations such as ST_Buffer, dense ST_Intersects matrices, and full-table topology validation balloon memory on large polygon sets or high-density point clouds. The boundary is a hard cap on the working set: pre-filter with an R-tree or Quadtree, stream features in bounded batches, and let each batch fall out of scope before the next loads. The pattern pairs naturally with mocking geospatial data for tests, since synthetic fixtures let you exercise the cap deterministically without pulling production-sized dumps onto a shared runner.

import shapely
from shapely import STRtree

def validate_in_batches(geoms, predicate_geom, batch=10_000):
    tree = STRtree(geoms)                       # R-tree pre-filter
    candidates = tree.query(predicate_geom)     # only nearby features
    for start in range(0, len(candidates), batch):
        window = candidates[start:start + batch]  # bounded working set
        yield shapely.intersects(geoms.take(window), predicate_geom)

A Runnable Boundary Suite with pytest

Boundary checks belong in the same harness as the rest of your assertions so they share fixtures, tolerance config, and failure reporting. The suite below loads tolerance from config, exercises all three metric families, and fails fast. It assumes Shapely 2.x and pytest 7+, and it mirrors the unit-versus-integration split covered in when to use unit vs integration tests in GIS — these boundary predicates are pure and in-memory, so they run on every commit.

import shapely
import pytest

# tolerance_config.yaml -> loaded once per session
TOLERANCE = {"epsg_4326_deg": 1e-6, "epsg_25832_m": 1e-3}

@pytest.fixture(scope="session")
def eps():
    return TOLERANCE["epsg_4326_deg"]

def test_tenant_isolation(tenant_a, tenant_b):
    # Hard boundary: zero tolerance for cross-tenant overlap.
    assert shapely.disjoint(tenant_a, tenant_b)

def test_transform_precision(source_geom, reprojected_geom, eps):
    # Symmetric boundary: drift must stay under the CRS-scaled epsilon.
    assert shapely.hausdorff_distance(source_geom, reprojected_geom) <= eps

def test_redaction_coarsens_below_grid(sensitive_point):
    redacted = shapely.set_precision(sensitive_point, grid_size=100.0)  # metres
    # Boundary: redacted output must not equal the original vertex.
    assert not sensitive_point.equals_exact(redacted, tolerance=0.0)

Run the family under a Great Expectations 0.18 custom expectation when the checks must live beside data-quality docs rather than code tests; the predicate is identical, only the wrapper differs.

PostGIS Boundary Counterparts

Server-side enforcement complements the Python suite by catching violations the moment data is written, before any client reads it. The relational predicates translate directly, and a CHECK or trigger turns the boundary into a constraint the database itself refuses to break. Pin libgeos and PROJ versions in the image so the same ST_* semantics run in CI and production.

-- Isolation: reject any insert whose geometry crosses a foreign tenant extent.
ALTER TABLE features ADD CONSTRAINT no_cross_tenant
  CHECK (ST_Disjoint(geom, (SELECT extent FROM foreign_tenant_bounds)));

-- Precision: a reprojection view that flags drift past tolerance.
SELECT f.id
FROM features f
JOIN features_4326 s ON s.id = f.id
WHERE ST_HausdorffDistance(ST_Transform(f.geom, 4326), s.geom) > 1e-6;  -- degrees

-- Redaction: coarsen sensitive points to a 100 m grid on egress.
SELECT id, ST_SnapToGrid(geom, 100.0) AS geom FROM sensitive_assets;

Pipeline Integration

Boundary predicates only protect production if they sit on a blocking gate. Wire the pytest suite into the pre-merge job so an isolation or precision failure rejects the merge, and run the heavier batched resource checks as a nightly job against staging-scale fixtures. Pin the geometry stack in the container — libgeos, PROJ, and GDAL — because a minor PROJ bump can shift a datum transform by centimetres and quietly trip a tolerance boundary. Emit structured logs (boundary_family, predicate, tolerance, crs, result) so observability tooling can alert on a rising assertion-failure rate before a rollback is needed. This gating shape is shared across the site’s spatial test pattern design and implementation work, and the synthetic inputs it depends on come from test data generation and mocking strategies; the tiered placement of cheap-versus-expensive checks follows the GIS test pyramid, and the CRS gate itself is detailed in automating CRS validation in CI pipelines.

Common Failure Modes and Gotchas

  1. CRS unit mismatch in the epsilon. A 1e-6 tolerance is ~0.11 m in EPSG:4326 but a tenth of a millimetre in a metre-based projection — applying a degree epsilon to projected coordinates makes every drift check pass. Always key tolerance on the CRS unit.
  2. DE-9IM boundary-touching slip. ST_Intersects returns true for geometries that merely share an edge, so two tenants touching along a shared border read as overlapping. Use ST_Disjoint for isolation and ST_Overlaps/ST_Crosses when interior overlap is the real concern.
  3. Snap-to-grid sliver artifacts. Redaction via set_precision/ST_SnapToGrid can collapse a thin polygon into an invalid or empty geometry; assert validity after coarsening rather than assuming the output is still a usable feature.
  4. Anti-meridian-spanning bounding boxes. A naive tenant envelope across ±180° longitude expands to nearly the whole globe, so the isolation check silently permits every join. Split the geometry at the anti-meridian or test in a projected CRS.
  5. Unbounded buffer before the pre-filter. Calling ST_Buffer on the full table before applying the R-tree query reintroduces the OOM the resource boundary was meant to prevent — filter first, buffer the survivors.
  6. Tolerance config drifting from container PROJ. Pinning the epsilon but not the PROJ/GDAL version lets a base-image bump move transforms underneath a fixed threshold; version both together.

Conclusion

Security boundaries become trustworthy only when they are deterministic, versioned, and executed on every commit instead of inspected after deployment. By separating isolation, precision, resource, and redaction boundaries — each with its own predicate and CRS-aware tolerance — a spatial QA suite catches the silent leaks and drift that generic security review misses. For the wider architecture these boundaries plug into, return to Geospatial QA Fundamentals & Architecture.