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.

Three tiers of quality, and what each one is allowed to do to a publishQuality splits into three tiers with different powers. Validity is intrinsic and objective — a ring either closes or it does not — and blocks publication outright. Fitness is relational, measured and published without ever stopping a publish, because no threshold makes a dataset universally good. Drift is a comparison against the layer own history and raises a producer-facing alert. Two of the three drop onto rails showing the failure of misassigning them: gating on fitness blocks data most consumers would use, and treating validity as advisory ships known defects.Validityobjective, intrinsicFitnessrelational, measuredDriftagainst own historyPublishedindicators travel with itblocksinformsalertsadvisory → defects shipgated → good data blockedThe two misassignmentsand both are common

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

The indicator set, by what enforces it and what it cannot seeSix indicators against their gate and their blind spot. Geometry validity rate blocks publication and says nothing about whether the geometry is in the right place. CRS conformance blocks and cannot detect a wrong datum transformation. Coordinate precision blocks and says nothing about accuracy. Positional accuracy is advisory and cannot see invalid geometry. Attribute completeness is advisory and cannot see a wrong value. Repaired-feature ratio is a drift signal with no meaning as a single measurement.GateBlind togeometry_validity_rateblockingwhether it is in the right placecrs_conformanceblockinga wrong datum transformationcoordinate_precisionblockingaccuracypositional_accuracy_rmseadvisoryinvalid geometryattribute_completenessadvisorya wrong valuerepaired_feature_ratiodriftanything, as one reading

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.

sql
-- 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.

python
# 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:

bash
# 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

  1. Confirm which indicator failed, not merely that publication was blocked. Run publish_gate.py with the partition in question and read the BLOCKED: list. A gate that reports only “quality failure” costs a producer an hour of bisecting; the named indicator costs a minute.
  2. Check whether the failure is in the data or in the declaration. A crs_conformance failure 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.
  3. 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.
  4. Decide repair versus rejection deliberately. ST_MakeValid fixes 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.
  5. Check the drift trend before treating an advisory breach as an incident. A repaired_feature_ratio of 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.
  6. 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.
  7. 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

Accuracy measurement methods, by what they can actually claimFour measurement methods and the tightest positional claim each supports, in metres, against a 2 metre product target. Surveyed control points support a claim as tight as the control set own accuracy, around 0.05 metres. Agreement against a known-accuracy reference layer supports a claim around 1.2 metres and measures agreement rather than truth. Internal consistency across tile seams supports about 0.4 metres and detects processing error only. Declared provenance measures nothing and must be labelled as inherited.surveyed control points0.05 mabsolute accuracyinternal consistency0.4 mprocessing error onlyreference layer agreement1.2 magreement, not truthdeclared provenance3 mmeasures nothingproduct target

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.