Compliance Reporting and Statutory Boundary Validation in PostGIS
Every excavation and watching brief carried out inside or near a designated heritage asset triggers a statutory obligation: features and finds must be tested against scheduled monument boundaries, conservation area limits, and listed-building curtilage, and the results must be reported to the planning authority in a defensible, reproducible form. Doing this by eye in a desktop GIS does not scale past a handful of features and leaves no audit trail. This section of the Artifact & Feature Spatial Database Design framework shows how to ingest statutory designation layers into PostGIS, validate spatial features against them with ST_Contains, ST_Intersects, and ST_DWithin, and emit constraint-search reports and submission packages that a curatorial officer can accept without re-checking your geometry. The four guides below drill into the concrete tasks: validating features against scheduled monument boundaries, exporting planning authority submission packages, automating heritage constraint search reports, and scheduling automated boundary validation jobs.
Statutory designation data in England reaches you as vector layers from Historic England’s National Heritage List for England (NHLE), from local authority Historic Environment Records (HERs), and from Ordnance Survey conservation area extracts. Each carries its own attribute schema, its own currency, and — critically — its own coordinate reference system, usually EPSG:27700 (British National Grid) but occasionally supplied in EPSG:4326. Validation is only meaningful when every layer and every feature you test shares one projected CRS, because ST_DWithin distances are expressed in the CRS unit (metres for BNG), and a WGS84 comparison returns degrees.
Prerequisites
| Library / tool | Version | Purpose |
|---|---|---|
| PostgreSQL | 16 | Relational engine hosting the spatial database |
| PostGIS | 3.4 | Spatial predicates, geometry storage, indexing |
| psycopg | 3.2.1 | Python driver for parameterised SQL execution |
| geopandas | 1.0.1 | Reading designation layers, spatial joins in Python |
| pyproj | 3.6.1 | CRS resolution and reprojection |
| GDAL/ogr2ogr | 3.9.1 | Loading and exporting designation layers |
| shapely | 2.0.5 | In-memory geometry predicates for pre-checks |
Assumptions: you have a PostGIS database with excavation and feature geometries already loaded (see PostGIS schema design for excavation units), write access to a compliance schema, and current designation extracts licensed for your organisation. All examples use EPSG:27700; substitute your site’s EPSG where marked.
Implementation
The workflow runs field-to-archive: ingest designations, normalise CRS, validate features, record violations, then report and export.
1. Create a dedicated compliance schema
Isolating designation layers and validation output in their own schema keeps statutory reference data separate from your interpretive excavation records, and lets you grant read-only access to reporting jobs.
-- PostgreSQL 16 / PostGIS 3.4
CREATE SCHEMA IF NOT EXISTS compliance;
CREATE TABLE IF NOT EXISTS compliance.designations (
designation_id BIGSERIAL PRIMARY KEY,
list_entry VARCHAR(20), -- NHLE reference, e.g. '1012345'
designation_type VARCHAR(40) NOT NULL, -- scheduled_monument | conservation_area | listed_building_curtilage
name TEXT,
grade VARCHAR(8), -- I, II*, II for listed buildings
source VARCHAR(80), -- 'Historic England NHLE 2026-06'
geom GEOMETRY(MultiPolygon, 27700) NOT NULL,
CONSTRAINT designations_valid CHECK (ST_IsValid(geom))
);
CREATE INDEX IF NOT EXISTS idx_designations_geom
ON compliance.designations USING GIST (geom);
CREATE INDEX IF NOT EXISTS idx_designations_type
ON compliance.designations (designation_type);
2. Ingest designation layers with ogr2ogr
Load each source layer directly into the designations table, forcing the target SRID and multipolygon type so a mix of Polygon and MultiPolygon sources coexists. The -nlt PROMOTE_TO_MULTI flag prevents the classic ERROR: Geometry type (Polygon) does not match column type (MultiPolygon) failure.
# GDAL 3.9.1 / ogr2ogr
ogr2ogr -f PostgreSQL \
PG:"host=localhost dbname=archaeology_db user=heritage" \
scheduled_monuments.gpkg \
-nln compliance.designations_stage \
-t_srs EPSG:27700 \
-nlt PROMOTE_TO_MULTI \
-lco GEOMETRY_NAME=geom \
-overwrite
Then map staged columns into the canonical schema so every source shares one attribute contract:
-- PostgreSQL 16 / PostGIS 3.4
INSERT INTO compliance.designations
(list_entry, designation_type, name, source, geom)
SELECT list_entry, 'scheduled_monument', name,
'Historic England NHLE 2026-06',
ST_Multi(ST_MakeValid(geom))
FROM compliance.designations_stage;
3. Normalise feature CRS before validation
Features arriving from total station or RTK-GNSS exports may sit in a local grid. Reproject to the designation CRS in Python before any predicate runs.
# requirements.txt
# geopandas==1.0.1
# pyproj==3.6.1
# psycopg==3.2.1
import geopandas as gpd
TARGET_EPSG = 27700 # British National Grid; substitute your site's EPSG
def normalise(gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
if gdf.crs is None:
raise ValueError("Feature layer has no CRS; reconcile against site datum first.")
if gdf.crs.to_epsg() != TARGET_EPSG:
gdf = gdf.to_crs(epsg=TARGET_EPSG)
return gdf
4. Run the core validation predicates
Three predicates cover most statutory tests: ST_Intersects flags any feature that touches or overlaps a designation, ST_Contains isolates features wholly inside a boundary, and ST_DWithin catches features within a proximity buffer (e.g. 50 m of a scheduled monument, a common consultation trigger).
-- PostgreSQL 16 / PostGIS 3.4
SELECT f.feature_id,
d.list_entry,
d.designation_type,
ST_Contains(d.geom, f.geom) AS fully_inside,
ST_Intersects(d.geom, f.geom) AS intersects,
ST_DWithin(d.geom, f.geom, 50) AS within_50m,
round(ST_Distance(d.geom, f.geom)::numeric, 2) AS distance_m
FROM features f
JOIN compliance.designations d
ON ST_DWithin(d.geom, f.geom, 50) -- index-assisted proximity join
WHERE d.designation_type = 'scheduled_monument';
The ST_DWithin join condition is index-assisted, so the GIST index on geom narrows candidates before the exact distance is computed — far cheaper than a Cartesian ST_Distance < 50.
5. Persist violations to an auditable table
Materialise results so a report reflects a fixed run, not a live query that shifts as data changes. The feature validation guide covers the violations table in depth.
-- PostgreSQL 16 / PostGIS 3.4
CREATE TABLE IF NOT EXISTS compliance.boundary_violations (
violation_id BIGSERIAL PRIMARY KEY,
feature_id VARCHAR(40) NOT NULL,
list_entry VARCHAR(20),
rule VARCHAR(40) NOT NULL, -- 'inside_scheduled' | 'within_50m'
distance_m NUMERIC(10,2),
detected_at TIMESTAMPTZ NOT NULL DEFAULT now(),
run_id UUID NOT NULL
);
CRS & Geometry Validation
Because ST_DWithin and ST_Distance return values in the CRS unit, mixing SRIDs is the single most common failure. PostGIS refuses cross-SRID operations outright with ERROR: Operation on mixed SRID geometries, which is preferable to a silent wrong answer, but it means every layer must be reprojected on ingest, not at query time. Confirm alignment before validating:
-- PostgreSQL 16 / PostGIS 3.4
SELECT DISTINCT ST_SRID(geom) FROM compliance.designations; -- expect 27700
SELECT DISTINCT ST_SRID(geom) FROM features; -- expect 27700
Designation polygons from public downloads frequently contain self-intersections and ring-order errors that make ST_Contains return unexpected results. Repair them once, on ingest, with ST_MakeValid, and verify with ST_IsValidReason to see the exact defect:
SELECT list_entry, ST_IsValidReason(geom)
FROM compliance.designations
WHERE NOT ST_IsValid(geom);
-- e.g. 'Self-intersection[529412 181004]'
Automated QA/QC
A validation run should be reproducible and gated. Assign each run a run_id (UUID), record the count of features tested and violations found, and refuse to promote a report if the designation source is older than your organisation’s currency threshold (Historic England updates the NHLE frequently; stale extracts miss new schedulings). Route three outcomes distinctly: clean features (logged, no action), proximity flags (reported for consultation), and containment violations (escalated). The scheduling guide builds this into a nightly job with an audit log keyed on run_id, so any historical report can be regenerated exactly.
Integration with adjacent workflows
Validation consumes the geometries defined by your schema and the spatial predicates modelled elsewhere. The proximity and containment joins here reuse patterns from Spatial Relationship Modeling in Heritage DBs, where ST_Touches, ST_Contains, and ST_DWithin are formalised as context rules. On large multi-season datasets the ST_DWithin designation join can dominate runtime, so the index and planner strategies in Query Optimization for Large Excavation Datasets apply directly to the validation query. Downstream, the persisted violations feed the constraint search report and the submission package export.
Troubleshooting
ERROR: Operation on mixed SRID geometries— a designation layer and the feature layer carry different SRIDs. Reproject on ingest with-t_srs EPSG:27700inogr2ogr, orST_Transform(geom, 27700)in SQL. Never compare geometries across SRIDs.ERROR: Geometry type (Polygon) does not match column type (MultiPolygon)— the source layer mixes singular and multi geometries. Add-nlt PROMOTE_TO_MULTItoogr2ogr, or wrap inserts inST_Multi().ST_Containsreturns false for a feature clearly inside — the designation polygon is invalid. RunSELECT ST_IsValidReason(geom)to locate the self-intersection, then repair withST_MakeValid().ST_DWithindistances look absurd (e.g. 0.0004) — geometries are inEPSG:4326; the “distance” is in degrees. Reproject to a metric CRS before any distance test.- Proximity join is slow on a full county HER — the planner is not using the GIST index. Confirm with
EXPLAIN ANALYZE, thenANALYZE compliance.designations;to refresh statistics after bulk load. psycopg.errors.InsufficientPrivilege: permission denied for schema compliance— the reporting role lacksUSAGE; grantGRANT USAGE ON SCHEMA compliance TO report_role;.
Compliance notes
Statutory reporting carries metadata obligations beyond the geometry itself. Record, per validation run: the designation source and its publication date (traceable to Historic England or the HER), the CRS authority (EPSG:27700), and the run timestamp — these satisfy the lineage and temporal-extent elements of ISO 19115. Populate MIDAS Heritage monument identifiers (the NHLE list entry number) so each violation links back to the national record. Align file naming and attribute definitions with the local authority’s deposition guidelines, and keep every input and output under a persistent identifier to meet FAIR findability and reusability. The report itself should state which designation currency was used, so a curatorial officer can judge whether re-validation is required.
Related
- Validating Features Against Scheduled Monument Boundaries — flag features inside or near designated areas and write a violations table.
- Exporting Planning Authority Submission Packages — assemble a GeoPackage plus PDF/CSV deliverable with a metadata sidecar.
- Automating Heritage Constraint Search Reports — intersect a site boundary against every designation layer and generate a report.
- Scheduling Automated Boundary Validation Jobs — re-run validation nightly with idempotency, alerting, and an audit log.
- Spatial Relationship Modeling in Heritage DBs — the predicate patterns the validation joins are built on.
- Query Optimization for Large Excavation Datasets — index and planner tuning for the proximity joins used here.