Spatial Data Quality and Validation Standards
Quality is a published property of a spatial data product, not an internal habit of the team that produces it.
A federated mesh removes the one thing a centralized GIS could always fall back on: a single team that knew, informally, which datasets were trustworthy. When twelve domains publish independently, that knowledge does not scale and cannot be recovered by asking. The replacement is a quality contract — a set of measured, published indicators that a consumer can read before deciding whether a product is fit for their use, and that a domain is accountable for holding. This topic sits inside Geospatial Data Mesh Fundamentals and specifies what those indicators are, how they are measured, and where in the pipeline each one is enforced. It pairs closely with Data Contracts for Spatial Products, which defines the promise; quality is the evidence that the promise is being kept.
Architectural Boundaries & Design Rationale
The boundary that matters here is between validity and fitness, and conflating them is the most common design error in spatial quality work.
Validity is intrinsic and objective. A polygon either closes or it does not; a ring is either correctly oriented or it is not; a coordinate either falls inside the declared CRS’s domain or it does not. These questions have one right answer, they can be evaluated without knowing anything about the consumer, and they should be enforced as hard gates that block publication. A product that publishes self-intersecting geometry has shipped a defect, and no consumer is served by receiving it.
Fitness is relational. A road centreline accurate to five metres is entirely fit for a regional logistics planner and entirely unfit for a utility crew locating a buried cable. There is no threshold that makes the dataset “good”; there is only a measurement, published, against which each consumer decides. Trying to gate on fitness produces one of two failures: a threshold set for the most demanding consumer, which blocks publication of data most consumers would happily use, or one set for the median consumer, which silently ships data that is wrong for the demanding ones.
The design consequence is a two-tier system. Validity checks are blocking and live in the publication path. Fitness measurements are advisory, computed at publication and carried in the catalog entry, where they inform consumer choice without ever stopping a publish. This is why quality metrics belong on the product’s metadata rather than in a monitoring dashboard — the dashboard tells the producer how they are doing; the catalog entry tells the consumer whether to use it.
A third category sits between them: drift. Neither validity nor fitness is a fixed property, and the interesting signal is usually a trend rather than a value. A layer whose repaired-feature ratio has climbed from 0.2% to 3% over two months is telling you something about its upstream source that no single measurement would reveal. Drift detection is therefore its own concern, running over the history of measurements rather than over the data, and its output is a producer-facing alert rather than a consumer-facing metric. The boundary discipline that keeps a domain’s internals private, described in Spatial Domain Boundary Design, applies here too: consumers see the measurement, not the pipeline that produced it.
Specification & Contract Reference
Every published spatial product carries the following quality block in its manifest. The gate column states where the indicator is enforced, and blocking indicators are the only ones that can prevent a publish.
| Indicator | Unit | Measured by | Gate | Typical target |
|---|---|---|---|---|
geometry_validity_rate |
fraction | ST_IsValid over all features |
Blocking | 1.0 |
ring_orientation_conformance |
fraction | ST_ForcePolygonCW comparison |
Blocking | 1.0 |
crs_conformance |
boolean | Declared SRID vs. observed coordinate domain | Blocking | true |
coordinate_precision |
decimal places | Max significant digits observed | Blocking | ≤ declared |
positional_accuracy_rmse |
metres | RMSE against control points | Advisory | Published, not gated |
attribute_completeness |
fraction | Non-null over required fields | Advisory | > 0.98 |
topological_consistency |
fraction | Shared-edge agreement across a coverage | Advisory | > 0.995 |
sliver_ratio |
fraction | Features below an area threshold | Advisory | < 0.001 |
vertex_density |
vertices/km | Total vertices over feature length | Advisory | Published |
repaired_feature_ratio |
fraction | Features altered by ST_MakeValid |
Drift | Trend, not threshold |
duplicate_geometry_ratio |
fraction | Exact-geometry duplicates | Advisory | < 0.0005 |
Two conventions make these comparable across domains. First, every rate is computed over the whole partition, not a sample — invalid geometry clusters around merge seams and clip edges, so a random sample is the method most likely to miss it. Second, positional accuracy is always reported as RMSE in metres against a named control set, never as a qualitative grade, and the control set identifier is part of the measurement. Two products reporting “high accuracy” are not comparable; two reporting rmse: 1.4m against control:national-geodetic-2024 are.
Precision deserves a specific note because it is routinely over-declared. A coordinate carried to fifteen decimal places in EPSG:4326 implies sub-nanometre precision, which no survey supports and no storage format meaningfully preserves. Declaring and enforcing a precision bound — six decimal places is roughly 0.1 m at the equator — makes equality comparisons meaningful, shrinks artifacts substantially, and prevents the false-difference class of bugs where two representations of the same point differ in noise digits.
Production Implementation
The validity gate runs in PostGIS at publication time, over the whole partition, and reports every indicator in one pass so a producer sees the complete picture rather than the first failure.
-- quality_report.sql — one pass over a partition, blocking + advisory indicators.
-- Parameters: :layer (text), :partition (text), :declared_srid (int), :max_precision (int)
WITH features AS (
SELECT feature_id, geom, attrs
FROM domain_features
WHERE layer = :'layer' AND partition_key = :'partition'
),
checks AS (
SELECT
count(*) AS total,
count(*) FILTER (WHERE ST_IsValid(geom)) AS valid_geom,
count(*) FILTER (WHERE ST_SRID(geom) = :declared_srid) AS crs_ok,
-- A sliver is a polygon whose area is negligible against its perimeter.
count(*) FILTER (
WHERE ST_GeometryType(geom) IN ('ST_Polygon', 'ST_MultiPolygon')
AND ST_Area(geom::geography) < 1.0
) AS slivers,
count(*) FILTER (WHERE attrs ? 'required_ref') AS complete_attrs,
sum(ST_NPoints(geom)) AS vertices,
-- Precision: significant decimals actually present in the WKT.
max(length(split_part(split_part(ST_AsText(geom), '.', 2), ' ', 1))) AS max_decimals
FROM features
)
SELECT
total,
round(valid_geom::numeric / nullif(total, 0), 6) AS geometry_validity_rate,
round(crs_ok::numeric / nullif(total, 0), 6) AS crs_conformance,
round(slivers::numeric / nullif(total, 0), 6) AS sliver_ratio,
round(complete_attrs::numeric / nullif(total, 0), 6) AS attribute_completeness,
vertices,
max_decimals,
-- Blocking verdict: any of these false stops the publish.
(valid_geom = total AND crs_ok = total AND max_decimals <= :max_precision) AS publishable
FROM checks;
The publish step consumes that report and refuses to proceed unless publishable is true, writing every indicator — blocking and advisory alike — into the catalog entry.
# publish_gate.py — quality is measured once, gates the publish, and travels with the product.
import json
import subprocess
import sys
BLOCKING = ("geometry_validity_rate", "crs_conformance")
def run_quality_report(layer: str, partition: str, srid: int, max_precision: int) -> dict:
"""Execute the one-pass report and return it as a dict. Idempotent: reading
quality never mutates the partition, so this is safe to retry."""
out = subprocess.run(
[
"psql", "--no-psqlrc", "--tuples-only", "--csv", "-f", "quality_report.sql",
"-v", f"layer={layer}", "-v", f"partition={partition}",
"-v", f"declared_srid={srid}", "-v", f"max_precision={max_precision}",
],
capture_output=True, text=True, check=True,
)
header, row = out.stdout.strip().splitlines()[:2]
return dict(zip(header.split(","), row.split(",")))
def gate(report: dict) -> None:
"""Blocking indicators fail closed: the previously published version stays live."""
failures = [k for k in BLOCKING if float(report.get(k, 0)) < 1.0]
if report.get("publishable") != "t":
failures.append("precision_bound")
if failures:
print(f"BLOCKED: {', '.join(failures)}", file=sys.stderr)
raise SystemExit(1)
if __name__ == "__main__":
layer, partition = sys.argv[1], sys.argv[2]
report = run_quality_report(layer, partition, srid=4326, max_precision=6)
gate(report) # zero-trust: never publish unvalidated
# Advisory indicators are published, not enforced — the consumer decides fitness.
print(json.dumps({"quality": report}, indent=2))
Verify the gate genuinely blocks by feeding it a partition with a known defect:
# Inject one self-intersecting polygon into a scratch partition, then run the gate.
psql -c "INSERT INTO domain_features (layer, partition_key, geom, attrs)
VALUES ('parcels','scratch',
ST_GeomFromText('POLYGON((0 0, 2 2, 2 0, 0 2, 0 0))', 4326),
'{\"required_ref\":\"x\"}'::jsonb);"
python3 publish_gate.py parcels scratch; echo "exit=$?" # expect BLOCKED, exit=1
Diagnostic Runbook
- Confirm which indicator failed, not merely that publication was blocked. Run
publish_gate.pywith the partition in question and read theBLOCKED:list. A gate that reports only “quality failure” costs a producer an hour of bisecting; the named indicator costs a minute. - Check whether the failure is in the data or in the declaration. A
crs_conformancefailure is ambiguous between “the geometry is in the wrong CRS” and “the manifest declares the wrong CRS”. Compare a known control point’s coordinates against its expected position: if it lands where the declared CRS says it should, the manifest is right and the data is wrong. - Localise the invalid features before repairing anything.
SELECT feature_id, ST_IsValidReason(geom) FROM domain_features WHERE NOT ST_IsValid(geom)names both the feature and the defect. Invalid geometry clusters, so the reasons are usually identical across the set — one upstream cause, not many. - Decide repair versus rejection deliberately.
ST_MakeValidfixes self-intersections and unclosed rings, but it can change geometry type — a self-intersecting polygon may become a collection. Where downstream consumers assume a polygon type, an automatic repair is a contract violation dressed as a fix. - Check the drift trend before treating an advisory breach as an incident. A
repaired_feature_ratioof 2% is meaningless in isolation and alarming if it was 0.1% last month. Query the last six publications of the same layer before escalating. - For a positional-accuracy dispute, verify the control set first. Two teams disagreeing about accuracy are usually measuring against different control sets, or the same set in different datums. Confirm both the control identifier and its CRS before investigating the data.
- If the gate passed but consumers report bad data, look at fitness, not validity. A perfectly valid layer at 5 m accuracy will fail a consumer needing 0.5 m. The fix is a conversation about the published indicator, not a repair.
What to Measure When There Is No Control Set
Positional accuracy against surveyed control points is the gold standard and is frequently unavailable — the control survey does not exist, is out of date, or costs more than the product is worth. A domain in that position has three usable options, and picking one deliberately is better than publishing no accuracy indicator at all.
Relative accuracy against a reference layer compares the product against another dataset already in the mesh whose accuracy is known. This measures agreement rather than truth: a road centreline that agrees with the cadastral layer to within 1.2 m is not necessarily 1.2 m from reality, but it is 1.2 m from the thing consumers will join it against, which is often the more useful number. The reference layer’s identifier and its own accuracy must both be published, or the figure is uninterpretable.
Internal consistency measures the product against itself — whether repeated observations of the same feature across acquisition dates land in the same place, whether adjacent tiles agree at their seams, whether a feature’s geometry is stable across republishes. This detects processing error and datum inconsistency without any external reference, and it is the only method available for a domain covering an area with no geodetic infrastructure.
Declared provenance accuracy propagates the accuracy the source claims, with the transformations applied recorded alongside it. This is the weakest option and is honest only if it is labelled as inherited rather than measured. Its value is that it prevents the worst outcome, which is a product with no accuracy statement at all — because a consumer facing silence will assume whatever their use case needs.
| Method | Measures | Needs | Publish as |
|---|---|---|---|
| Control points | Absolute accuracy | A current control survey | rmse + control set id |
| Reference layer | Agreement with the mesh | A known-accuracy neighbour | rmse + reference product id |
| Internal consistency | Processing stability | Repeat observations or seams | seam_disagreement + method |
| Declared provenance | Nothing measured | The source’s own claim | inherited_accuracy + source |
Whichever is used, the method identifier is part of the measurement. An rmse of 1.4 m means something very different measured against national control than measured against a neighbouring layer, and a consumer who cannot tell which will use the number as though it were the first.
SLA Targets & Performance Baselines
| Metric | Target | Alert threshold | Remediation |
|---|---|---|---|
| Blocking-gate pass rate | > 99% of publishes |
< 95% over 7 days |
Investigate the upstream producer’s export path |
| Quality report runtime | < 90s per partition |
> 240s |
Re-check the spatial index; partition may be oversized |
repaired_feature_ratio drift |
< 2× the 90-day median |
> 5× |
Producer-facing alert; source export settings changed |
| Advisory-indicator freshness | Computed on every publish | Missing on any publish | Publication path is bypassing the gate |
| Control-set revalidation | Annually | Overdue by 90 days | Re-survey or re-adopt a current control set |
| Consumer-reported defects | < 1 per product per quarter |
> 3 |
The published indicators do not reflect real fitness |
The last row is the one worth watching most closely. Consumer-reported defects on a product whose indicators all look healthy mean the indicator set is measuring the wrong things — the most common cause being a fitness dimension nobody thought to publish, such as temporal currency of individual features within an otherwise fresh layer.
One further point on where the gate runs. Quality checks belong in the publication path rather than in a scheduled sweep, because a sweep detects a defect that has already been published and consumed. A sweep is still worth running, but its job is different: it catches drift in data that was valid when published and has since been altered by a migration, a manual repair, or a schema change. The two are complementary and neither substitutes for the other — the gate protects consumers from new defects, the sweep protects them from old data quietly becoming wrong.
Governance & Compliance Notes
Quality indicators are audit evidence, and treating them as such changes how they are stored. Each measurement is recorded against the specific product version that was published, immutably, so a question about a decision made eighteen months ago can be answered with the indicators as they stood then rather than as they stand now. Overwriting a product’s quality block on republish destroys exactly the record an audit needs.
The governance body’s role is to define the indicator set and the control sets, not to review individual measurements. A quality council that approves publications becomes the bottleneck federation was meant to remove; one that maintains the shared definition of positional_accuracy_rmse and the registry of approved control sets lets every domain measure itself against a common standard without a queue. Where a domain needs an indicator the standard set does not cover — cloud fraction for imagery, temporal coverage for sensor feeds — it publishes it as a domain-specific extension under a namespaced key, and the council adopts it into the standard set only if a second domain finds it useful.
Jurisdictional constraints attach to accuracy as well as to content. Some jurisdictions restrict publication of positional data above a stated precision, which makes coordinate_precision a compliance control rather than merely a hygiene one — and makes over-declaring precision a potential violation rather than a cosmetic error. Where such a limit applies, it is enforced in the same blocking gate as validity, so a compliance breach cannot be published in the first place.
Related
- Data Contracts for Spatial Products — the promise that quality indicators supply evidence for
- Metadata Cataloging for Raster/Vector — where quality blocks are published and discovered
- Spatial Product Lifecycle Management — how quality gates differ by lifecycle state
- Topology Validation with Dagster Asset Checks — the blocking gate as an orchestrated asset check
- Geospatial Data Mesh Fundamentals — up to the section overview