Monitoring Precision Loss After Geometry Simplification
Simplification is how a spatial platform makes large geometries affordable: reduce a twelve-thousand-vertex coastline to four hundred vertices and every tile render, every spatial join and every transfer gets cheaper. It is also how a platform quietly changes its own answers. A tolerance chosen for zoom 6 applied at zoom 14 moves boundaries by tens of metres; an aggressive pass collapses a narrow inlet, turns a valid polygon into a self-touching one, or shifts a parcel edge across a road. The simplified layer is smaller, faster and subtly wrong, and no validity check notices because the output is still valid. This guide covers what to measure after a simplification pass, how to set a defensible tolerance per zoom, and how to catch the passes that changed more than they should. It belongs to geometry validity and topology checks under spatial data freshness and quality metrics.
Problem framing: three things a simplification can break
Vertex-count reduction is the intended effect. Three side effects are the ones worth measuring.
Displacement. Every simplified vertex sits some distance from the original outline. The tolerance bounds it in principle, but the bound applies to the algorithm’s own metric and the practical maximum can exceed what people assume — particularly with topology-preserving variants that trade a tighter guarantee for consistency between neighbours. Measuring the actual Hausdorff distance between the original and simplified geometry gives the number rather than the promise.
Area and shape change. A simplified polygon encloses a different area. On a parcel or administrative layer that matters directly: area is frequently an attribute consumers use, and a two-percent area change on a cadastral polygon is a data-quality incident even if nobody can see it on a map.
Feature destruction. Narrow inlets, isolated islands in a multipolygon, and thin peninsulas disappear entirely above a certain tolerance. Ring and part counts falling is the cheap detector for this, and it catches the failure that displacement measurement can miss — a removed island has no corresponding simplified geometry to measure distance against.
A fourth effect is worth naming because it undermines the others: topology breakage between neighbours. Simplifying adjacent polygons independently moves their shared edges apart, creating exactly the gaps and slivers that break spatial joins. Topology-preserving simplification exists to prevent this and should be the default for any coverage used in a join.
Implementation: measure the pass, not just its output
Compute the three signals in the same statement that produces the simplified geometry, so the comparison is against the exact input.
-- Simplify and measure in one pass. ST_SimplifyPreserveTopology avoids the
-- self-intersections plain ST_Simplify can produce, at some extra cost.
CREATE TABLE derived.districts_z10 AS
WITH s AS (
SELECT district_id,
geom AS original,
ST_SimplifyPreserveTopology(geom, 20.0) AS simplified
FROM curated.districts
)
SELECT
district_id,
simplified AS geom,
ST_NPoints(original) AS vertices_before,
ST_NPoints(simplified) AS vertices_after,
-- The real displacement, not the nominal tolerance.
ST_HausdorffDistance(original, simplified) AS max_displacement_m,
-- Area change as a signed ratio: negative means the polygon shrank.
(ST_Area(simplified) - ST_Area(original)) / NULLIF(ST_Area(original), 0)
AS area_delta_ratio,
-- Parts and rings destroyed: islands and holes that vanished.
ST_NumGeometries(original) - ST_NumGeometries(simplified) AS parts_lost,
ST_NRings(original) - ST_NRings(simplified) AS rings_lost
FROM s;
Aggregate those per layer and zoom into metrics, and keep the per-feature values available so the worst offenders can be inspected.
SELECT
COUNT(*) AS features,
ROUND(AVG(1 - vertices_after::numeric / vertices_before), 3) AS mean_reduction,
MAX(max_displacement_m) AS worst_displacement_m,
percentile_disc(0.99) WITHIN GROUP (ORDER BY max_displacement_m)
AS p99_displacement_m,
MAX(ABS(area_delta_ratio)) AS worst_area_delta,
SUM(GREATEST(parts_lost, 0)) AS parts_destroyed,
SUM(GREATEST(rings_lost, 0)) AS rings_destroyed
FROM derived.districts_z10;
Choosing a tolerance per zoom
Tie the tolerance to the ground resolution of the zoom level it serves, not to a number that produced a satisfying file size. A tile pixel at zoom near the equator covers roughly
and a simplification tolerance of half to one pixel is invisible at that zoom while removing most of the vertices.
| Zoom | Pixel ground size | Sensible tolerance | Typical vertex reduction |
|---|---|---|---|
| 6 | ~2 445 m | 1 200 m | 98% |
| 10 | ~153 m | 75 m | 92% |
| 12 | ~38 m | 20 m | 78% |
| 14 | ~9.5 m | 5 m | 55% |
| 16 | ~2.4 m | 1 m | 20% |
The important operational rule is that simplified geometry is for rendering, never for analysis. A pipeline that simplifies once and uses the result for both saves storage and corrupts every measurement downstream. Keep the full-precision geometry authoritative and treat every simplified variant as a derived rendering product with its own row in the layer registry.
Alerting
- alert: SimplificationDisplacementHigh
# Displacement should never exceed the zoom's pixel size; the registry
# supplies the expected value per layer and zoom.
expr: |
gis_spatial_simplify_p99_displacement_meters
> on (layer, zoom) group_left() gis_layer_registry_pixel_meters
for: 15m
labels: { severity: warning, data_domain: spatial }
- alert: SimplificationDestroyedParts
# An island or hole vanishing is always worth a human decision.
expr: increase(gis_spatial_simplify_parts_destroyed_total[1h]) > 0
for: 0m
labels: { severity: warning, data_domain: spatial }
- alert: SimplificationAreaDrift
expr: gis_spatial_simplify_worst_area_delta > 0.02
for: 15m
labels: { severity: warning, data_domain: spatial }
Verification
Simplify a known geometry — a rectangle with a deliberate notch — at increasing tolerances and confirm the measured displacement rises as expected and that the notch disappears at the tolerance where it should. This calibrates your intuition about what the tolerance means for your data, which is otherwise surprisingly hard to acquire.
Then confirm the derived layer is not being used for analysis. Grep the query layer for references to the simplified tables outside rendering paths; every one is a place where a measurement is being taken from a rendering product.
Gotchas
Plain ST_Simplify on polygons. Can produce self-intersections and invalid output. Use the topology-preserving variant for anything but throwaway line rendering.
Simplifying neighbours independently. Opens gaps and slivers along shared edges, producing the join failures described in finding slivers and gaps in adjacent polygon coverage. Simplify the shared topology, not the polygons.
Tolerance in degrees. On unprojected data the tolerance is in degrees, so a value of 20 is roughly two thousand kilometres. Reproject or use a geography-aware path.
Measuring reduction and nothing else. Vertex reduction is the benefit, not the risk. Displacement, area change and destroyed parts are the risk.
Using the simplified layer for area attributes. A cadastral area computed from a rendering geometry is wrong by whatever the tolerance cost, and it will be believed.
FAQ
Should the simplified layer be validated as well?
Yes, with the same validity gate as the source. Topology-preserving simplification is much less likely to produce invalid output, but “much less likely” is not a guarantee, and the cost of the check is trivial compared with rebuilding a pyramid from bad geometry.
How do I simplify a coverage without breaking shared edges?
Extract the shared topology first — the set of edges — simplify the edges once, then rebuild the polygons from the simplified edges. Every neighbour then shares an identical simplified boundary by construction. It is more work than simplifying polygons independently and it is the only approach that keeps a coverage joinable.
Does simplification interact with the tile size distribution?
Directly: tolerance is the main lever on tile size, and a change in tolerance shows up immediately in the size histogram described in raster and tile pipeline observability. If tile sizes move without a style change, an altered tolerance is the first thing to check.
What about simplifying for transfer rather than rendering?
The same measurements apply, and the tolerance should be driven by what the recipient will do with the data. Transferring a simplified layer to a consumer who will compute areas from it is the same mistake as using it internally for analysis, just with the consequences moved outside your platform.
How should a tolerance change be rolled out?
Treat it as a change to a derived product: build the new variant alongside the old, compare displacement, area delta and destroyed parts between them, and promote only if the new figures stay inside the zoom’s budget. Rolling a tolerance change straight into production is how a pyramid acquires a shoreline nobody recognises, and the comparison costs one extra build.
Is a lower vertex count always cheaper?
Not always. Topology-preserving simplification is itself expensive, and on a layer rebuilt frequently the simplification cost can exceed the rendering saving. Measure the pass duration alongside the reduction, and consider caching simplified geometry rather than recomputing it per build.
Keep the per-feature measurements for at least one previous build. When a rendering complaint arrives — a boundary that looks wrong at one zoom — the fastest answer is usually a comparison of that feature’s displacement across the last two builds, which is a lookup rather than an investigation.
Related
- Geometry validity and topology checks — the parent topic covering validity of the output.
- Finding slivers and gaps in adjacent polygon coverage — the defects independent simplification creates.
- Sampling telemetry for high-vertex polygons — the vertex-count signal that identifies simplification candidates.