Writing a Tolerance Policy Your Team Can Audit

The tolerance is the number that decides whether a shift is noise to be patched or an edit to be reviewed, and in most repositories it lives in a script, undocumented, set by somebody who has since moved on. This page is a focused companion to automated patching for minor geometry shifts.

Concept & Context

A tolerance is not a preference. It is a statement about how accurately the data was measured: below it, two coordinates that differ are the same point observed twice; above it, they are two different points. Written that way, the number is derivable rather than negotiable β€” and the derivation is what makes it auditable.

What goes wrong is almost always the same. A single tolerance is applied across a repository whose layers were captured by entirely different methods, so it is simultaneously too loose for the survey-grade layers and too tight for the digitised ones. Then, because the loose end produces complaints, somebody widens it β€” and a class of real edits silently stops being reported.

A policy fixes this by attaching each tolerance to a capture method, stating what happens on either side of it, and versioning the whole thing beside the data it governs. None of that is difficult; what it buys is the ability to answer β€œwhy is this number 25 mm” two years later without guessing.

Each tolerance derives from how the layer was captured A grid of four capture methods against their stated horizontal accuracy, the tolerance derived from it, and what the policy does with a displacement above that tolerance. Stated accuracy Tolerance Above it RTK GNSS survey 0.02 m 0.025 m escalate Differential GNSS 0.5 m 0.6 m escalate Digitised from ortho 0.25 m pixel 1.0 m patch and log Administrative source not measured none never patched A single number across all four rows is simultaneously too loose for the first and too tight for the third.

Core Algorithmic Pipeline

  1. Group layers by capture method β€” RTK survey, differential GNSS, digitised from orthophoto, derived from an administrative source.
  2. Derive one tolerance per group from the stated horizontal accuracy of that method, not from what makes the current diff quiet.
  3. State the escalation for each group: what happens above the tolerance, and what makes a layer exempt from automatic patching entirely.
  4. Version the policy file beside the data, and require a reason on every change.
  5. Make the pipeline read it β€” a policy the code does not consult is documentation.
What the policy decides for one measured displacement A decision ladder evaluated for each shift: a layer on the never-patch list escalates whatever the number is, an ungoverned layer stops the run, a displacement inside the group's tolerance is patched, and anything else follows the group's declared escalation. Is the layer on the never-patch list? Escalate no tolerance makes a registered boundary safe yes no Does the layer belong to no group? Stop the run a default would be a decision nobody made yes no Is the displacement inside the group's tolerance? Patch and log with the group, number and capture method yes no Follow the group's escalation escalate, or patch β€” as declared Two of the four outcomes are refusals, and both exist to stop the pipeline deciding something silently.

Working Implementation

# config/tolerance-policy.yml
# A tolerance describes how the data was measured. Changing one changes what the
# repository considers a real edit, so every change is reviewed like code.
version: 4
last_changed: "2026-06-18"
last_changed_reason: >-
  Cadastral survey equipment upgraded to RTK; tolerance tightened from 0.10 m to
  0.025 m in line with the new stated horizontal accuracy.

groups:
  rtk_survey:
    capture_method: "RTK GNSS, stated horizontal accuracy 0.02 m"
    tolerance_m: 0.025
    above_tolerance: escalate          # never patched automatically
    layers: [parcels, boundary_marks, control_network]

  dgps_field:
    capture_method: "Differential GNSS, stated horizontal accuracy 0.5 m"
    tolerance_m: 0.6
    above_tolerance: escalate
    layers: [utility_points, access_tracks]

  digitised_ortho:
    capture_method: "Heads-up digitising from 0.25 m orthophoto"
    tolerance_m: 1.0
    above_tolerance: patch_and_log     # low-stakes layers, reversible patches
    layers: [land_cover, vegetation]

# Sensitivity beats tolerance: no number makes these safe to patch automatically.
never_patch:
  reason: "Registered boundaries carry legal weight; every change is a decision."
  layers: [parcels, boundary_marks]
"""Read the policy, and refuse to run without one."""
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import yaml


@dataclass(frozen=True)
class Tolerance:
    group: str
    metres: float
    above: str
    capture_method: str
    never_patch: bool


class PolicyError(RuntimeError):
    pass


def load_policy(path: str = "config/tolerance-policy.yml") -> dict:
    doc = yaml.safe_load(Path(path).read_text(encoding="utf-8"))
    for required in ("version", "last_changed", "last_changed_reason", "groups"):
        if required not in doc:
            raise PolicyError(f"tolerance policy is missing {required!r}")
    return doc


def tolerance_for(layer: str, policy: dict) -> Tolerance:
    """The tolerance governing a layer, or a refusal to proceed.

    There is deliberately no default. A layer nobody assigned to a group is a
    layer nobody decided about, and guessing a tolerance for it is exactly the
    silent behaviour this policy exists to remove.
    """
    never = layer in policy.get("never_patch", {}).get("layers", [])

    for name, group in policy["groups"].items():
        if layer in group["layers"]:
            return Tolerance(
                group=name,
                metres=float(group["tolerance_m"]),
                above=group["above_tolerance"],
                capture_method=group["capture_method"],
                never_patch=never,
            )

    raise PolicyError(
        f"layer {layer!r} belongs to no tolerance group. Add it to a group in "
        "the policy β€” do not fall back to a default, because a tolerance that "
        "was never chosen cannot be defended."
    )


def decide(layer: str, displacement_m: float, policy: dict) -> tuple[str, str]:
    """(action, reason) for one measured displacement."""
    t = tolerance_for(layer, policy)

    if t.never_patch:
        return "escalate", f"{layer} is in never_patch: {policy['never_patch']['reason']}"
    if displacement_m <= t.metres:
        return "patch", (f"{displacement_m:.3f} m is within the {t.group} tolerance "
                         f"of {t.metres:.3f} m ({t.capture_method})")
    if t.above == "patch_and_log":
        return "patch", f"{displacement_m:.3f} m exceeds tolerance; patched under {t.group} policy"
    return "escalate", (f"{displacement_m:.3f} m exceeds the {t.group} tolerance "
                        f"of {t.metres:.3f} m")

Every returned reason names the group, the number and the capture method it came from. That string ends up in the audit record, which is what turns β€œthe pipeline moved this vertex” into β€œthe pipeline moved this vertex because 6 mm is inside the 25 mm RTK tolerance”.

Validation & Output Verification

# Every layer in the repository must be governed by exactly one group
import yaml, glob, os
from tolerance import load_policy, tolerance_for, PolicyError

policy = load_policy()
layers = {os.path.basename(p).removesuffix(".gpkg")
          for p in glob.glob("data/**/*.gpkg", recursive=True)}

assigned = [l for g in policy["groups"].values() for l in g["layers"]]
assert len(assigned) == len(set(assigned)), "a layer appears in two groups"

for layer in sorted(layers):
    try:
        t = tolerance_for(layer, policy)
        print(f"{layer:<20} {t.group:<16} {t.metres:.3f} m")
    except PolicyError as exc:
        raise SystemExit(f"ungoverned layer: {exc}")
# A tolerance must never be looser than the capture accuracy it claims to describe
python - <<'PY'
import re, yaml
policy = yaml.safe_load(open("config/tolerance-policy.yml"))
for name, group in policy["groups"].items():
    stated = float(re.search(r"([\d.]+)\s*m", group["capture_method"]).group(1))
    tol = float(group["tolerance_m"])
    assert tol <= stated * 3, (
        f"{name}: tolerance {tol} m is more than three times the stated capture "
        f"accuracy {stated} m β€” it will absorb real edits"
    )
    print(f"{name}: tolerance {tol} m against capture accuracy {stated} m")
PY

# A tolerance change must arrive on its own, with a reason
git log --format='%h %s' -- config/tolerance-policy.yml | head -5
git show --stat HEAD -- config/tolerance-policy.yml

Requiring a policy change to be its own commit is worth enforcing in review. A tolerance widened in the same commit as a data change produces a diff nobody can interpret: some of those features moved, and some only appear to have moved because the threshold did.

Failure Modes

  • A tolerance nobody can justify β€” symptom: the number is questioned and nobody knows where it came from. Root cause: it was set to make a diff quiet. Fix: derive it from the stated capture accuracy and record that derivation in the policy.

  • One tolerance across the repository β€” symptom: the same number is simultaneously too loose and too tight. Root cause: a single global constant. Fix: group by capture method, as above; the groups are usually three or four.

  • A layer nobody assigned β€” symptom: an unfamiliar layer is patched with a default. Root cause: a fallback value. Fix: refuse to run, which is what tolerance_for does.

  • A tolerance change hidden in a data commit β€” symptom: a diff in which real and threshold-induced changes are indistinguishable. Root cause: the policy is edited alongside the data. Fix: require the policy change as its own reviewed commit, and check for it in the pipeline.

A tolerance change in its own commit, or hidden in a data commit Two panels. A tolerance changed alongside data produces a diff in which real edits and threshold-induced ones are indistinguishable; changed on its own with a stated reason, the next data diff is interpretable again. HIDDEN IN A DATA COMMIT Some features moved; some only appear to have The reviewer cannot tell which is which The reason for the new number is not recorded Reverting the data does not revert the threshold ITS OWN REVIEWED COMMIT One diff showing exactly one number changing The reason and the capture method recorded with it The next data diff is interpretable again The change is revertible on its own Check for it in the pipeline: a commit touching both the policy and the data should fail. The same argument applies to the quantisation grid, and for the same reason.

Back to Automated Patching for Minor Geometry Shifts