Validating Features Against Scheduled Monument Boundaries
When an excavation trench or a recorded artifact falls inside — or within the consultation buffer of — a scheduled monument, that fact must be established mechanically, recorded, and traceable, not inferred from a supervisor squinting at a basemap. This guide implements the core test of the compliance reporting and statutory boundary validation workflow: given a table of feature geometries and a table of scheduled monument polygons in EPSG:27700, flag every feature that intersects a boundary or sits within a proximity buffer, and write the results to a durable violations table with enough context for a curatorial officer to act on.
Context & When to Use
Run this test whenever new feature geometry is committed on a site that overlaps or abuts a scheduled area, and always before submitting a report. The 50 m buffer is a pragmatic default reflecting the setting-consultation distances many curators apply, but it is a policy parameter, not a law — confirm the threshold with the relevant authority and make it configurable. The trade-off is between recall and reviewer workload: a wide buffer flags more features and burdens the officer, a narrow one risks missing a feature that genuinely affects the monument’s setting. Containment (ST_Contains) is unambiguous and always escalated; proximity (ST_DWithin) is advisory and drives consultation. Do the exact distance in the database, not in Python round-trips, so the GIST index does the culling.
Two predicate choices deserve care. Use ST_Contains(d.geom, f.geom) rather than ST_Within(f.geom, d.geom) for the containment test — they are logically equivalent with arguments swapped, but ordering the designation first keeps the code readable against the boundary being the reference frame. And prefer ST_DWithin over a hand-rolled ST_Buffer plus ST_Intersects: buffering every designation polygon on the fly is expensive and defeats the spatial index, whereas ST_DWithin uses the index directly and computes the true minimum distance without an intermediate geometry. A point find, a polyline section drawing, and a polygon trench all validate through the same query because these predicates are geometry-type agnostic.
Implementation
The violations table records one row per feature-designation-rule match, keyed to a run_id so a report reflects a fixed snapshot.
-- PostgreSQL 16 / PostGIS 3.4
CREATE TABLE IF NOT EXISTS compliance.boundary_violations (
violation_id BIGSERIAL PRIMARY KEY,
run_id UUID NOT NULL,
feature_id VARCHAR(40) NOT NULL,
list_entry VARCHAR(20), -- NHLE scheduled monument reference
monument TEXT,
rule VARCHAR(24) NOT NULL, -- 'inside_scheduled' | 'within_buffer'
distance_m NUMERIC(10,2) NOT NULL,
detected_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (run_id, feature_id, list_entry, rule)
);
The Python driver runs the test in one parameterised statement and inserts results in the same transaction, so a report never sees a half-written run.
# requirements.txt
# psycopg==3.2.1
# geopandas==1.0.1
# pyproj==3.6.1
import uuid
import psycopg
BUFFER_M = 50 # setting-consultation distance; confirm with the curator
TARGET_EPSG = 27700 # British National Grid; substitute your site's EPSG
VALIDATE_SQL = """
INSERT INTO compliance.boundary_violations
(run_id, feature_id, list_entry, monument, rule, distance_m)
SELECT %(run_id)s,
f.feature_id,
d.list_entry,
d.name,
CASE WHEN ST_Contains(d.geom, f.geom) THEN 'inside_scheduled'
ELSE 'within_buffer' END,
round(ST_Distance(d.geom, f.geom)::numeric, 2)
FROM features f
JOIN compliance.designations d
ON d.designation_type = 'scheduled_monument'
AND ST_DWithin(d.geom, f.geom, %(buffer)s) -- index-assisted prefilter
ON CONFLICT (run_id, feature_id, list_entry, rule) DO NOTHING;
"""
def validate_features(conn_str: str, buffer_m: float = BUFFER_M) -> tuple[str, int]:
"""Test every feature against scheduled monuments; return (run_id, violation_count)."""
run_id = str(uuid.uuid4())
with psycopg.connect(conn_str) as conn:
with conn.cursor() as cur:
# Guard against the classic mixed-SRID failure before running the join.
cur.execute("SELECT DISTINCT ST_SRID(geom) FROM features;")
srids = {row[0] for row in cur.fetchall()}
if srids != {TARGET_EPSG}:
raise ValueError(
f"features not uniformly EPSG:{TARGET_EPSG}: found SRIDs {srids}. "
"Reproject before validation."
)
cur.execute(VALIDATE_SQL, {"run_id": run_id, "buffer": buffer_m})
count = cur.rowcount
conn.commit()
return run_id, count
If you collapse them you will under-report to the planning authority.
Verification
After a run, confirm the counts and inspect the escalation-worthy rows. Containment violations should always have distance_m = 0 because a contained geometry has zero distance to its container.
-- PostgreSQL 16 / PostGIS 3.4
SELECT rule, count(*) AS n, min(distance_m), max(distance_m)
FROM compliance.boundary_violations
WHERE run_id = '<run-id-from-python>'
GROUP BY rule;
Expected output for a run flagging two contained and three nearby features:
rule | n | min | max
-----------------+---+------+-------
inside_scheduled| 2 | 0.00 | 0.00
within_buffer | 3 | 4.12 | 47.80
Assertion checks worth wiring into a test: every inside_scheduled row has distance_m = 0; every within_buffer row has 0 < distance_m <= BUFFER_M; and re-running the same run_id inserts zero new rows (the ON CONFLICT ... DO NOTHING idempotency guarantee).
Common Errors & Fixes
ERROR: Operation on mixed SRID geometries— thefeaturesanddesignationstables hold different SRIDs, so PostGIS refuses theST_DWithinjoin. The Python guard above catches this early; to fix the data,UPDATE features SET geom = ST_Transform(geom, 27700);or reload withogr2ogr -t_srs EPSG:27700.ERROR: GEOSContains: TopologyException: side location conflict— a designation polygon is invalid, andST_Containscannot evaluate it. Repair once withUPDATE compliance.designations SET geom = ST_MakeValid(geom) WHERE NOT ST_IsValid(geom);then re-run.- Every feature flagged
within_bufferat fractional distances — geometries are in degrees (EPSG:4326), so the 50-unit buffer spans continents. Confirm withSELECT ST_SRID(geom) FROM features LIMIT 1;and reproject to the metric BNG CRS before validating.
Related
- Compliance Reporting and Statutory Boundary Validation — the section overview this test belongs to.
- Automating Heritage Constraint Search Reports — turn violations into a narrative constraints report for a site boundary.
- Scheduling Automated Boundary Validation Jobs — run this validation nightly and alert on newly detected violations.
- Spatial Relationship Modeling in Heritage DBs — the containment and proximity predicates underpinning the join.
- PostGIS Schema Design for Excavation Units — how the feature geometries under test are structured and constrained.