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.
Core Algorithmic Pipeline
- Group layers by capture method β RTK survey, differential GNSS, digitised from orthophoto, derived from an administrative source.
- Derive one tolerance per group from the stated horizontal accuracy of that method, not from what makes the current diff quiet.
- State the escalation for each group: what happens above the tolerance, and what makes a layer exempt from automatic patching entirely.
- Version the policy file beside the data, and require a reason on every change.
- Make the pipeline read it β a policy the code does not consult is documentation.
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_fordoes. -
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.
Related
- Automated Patching for Minor Geometry Shifts β the parent guide and the patching this policy governs
- Tuning Tolerance Thresholds for Conflict Detection β measuring the thresholds this policy records
- Choosing a Coordinate Quantisation Grid for Delta Encoding β the same derivation applied to delta encoding
- Manual Review Triggers for Critical Edits β where an escalation from this policy is routed