Snapping Vertices to Tolerance in PostGIS
Two adjacent parcels that look like they share a boundary but whose vertices sit a few millimetres apart will fail every gap-and-overlap check you throw at them, and no amount of ST_MakeValid will close the seam — validity repair fixes single-feature defects, not misalignment between features. Those sub-tolerance gaps come from ordinary sources: independent digitising of the two parcels, coordinate truncation during a format conversion, or floating-point drift after a reprojection. The fix is snapping: nudging vertices onto a common grid or onto a neighbour’s edge within a controlled tolerance. This page covers the three PostGIS functions that do it — ST_SnapToGrid, ST_Snap, and ST_Node — how to pick a tolerance from the data, and how to snap without collapsing thin features or tearing topology. It is a sibling procedure to the ST_MakeValid repair guide within the automated geometry remediation stage. The critical discipline throughout is that snapping is a destructive operation — it rewrites coordinates — so the tolerance must be derived from the data and every snap must be verified rather than trusted.
Prerequisites
- PostGIS 2.x or newer.
ST_SnapToGrid,ST_Snap, andST_Nodeare long-established.ST_Noderelies on GEOS; a modern GEOS (3.8+) gives more robust noding on messy input. - A projected CRS in linear units. Tolerances and grid sizes are expressed in the geometry’s own units. On an EPSG:4326 layer a “tolerance” of
0.001is a thousandth of a degree — roughly 100 m at the equator — which is almost never what you intend. Work in a projected coordinate reference system (CRS) whose units are metres or feet. - A spatial index (
CREATE INDEX ... USING GIST (geom)) on any table you snap against a reference layer, or the neighbour lookups inST_Snapworkflows will do sequential scans. - Gotcha:
ST_Snapis directional and pairwise — it snaps its first argument toward its second within the tolerance. It does not globally reconcile a whole layer to itself. Aligning an entire coverage means snapping each feature to the union of its neighbours, not callingST_Snaponce on the table.
Step-by-Step Procedure
Step 1 — Measure the near-miss distances
Ground the tolerance in the data. Find how far apart vertices that should coincide actually are.
-- Distance from each parcel to its nearest neighbour edge (excluding self).
-- The distribution of small non-zero distances reveals the real gap size.
SELECT
round(ST_Distance(a.geom, b.geom)::numeric, 4) AS gap,
count(*) AS n
FROM parcels a
JOIN parcels b
ON a.id < b.id
AND ST_DWithin(a.geom, b.geom, 1.0) -- only near neighbours, in metres
WHERE NOT ST_Intersects(a.geom, b.geom)
GROUP BY gap
ORDER BY gap
LIMIT 20;
Verification: the smallest non-zero gap values (for example a concentration around 0.01–0.05 m) are your near-misses. Choose a tolerance just above that band and confirm it is far below your smallest genuine feature width.
Step 2 — Remove near-duplicate vertices with ST_SnapToGrid
Snap coordinates to a grid finer than any real feature detail. This collapses sub-tolerance duplicate vertices and pins coordinate precision to a stable value.
-- Snap all coordinates to a 1 mm grid (0.001 m) to stabilise precision
-- and collapse near-duplicate vertices within each feature.
UPDATE parcels
SET geom = ST_SnapToGrid(geom, 0.001)
WHERE id >= 0 AND id < 10000; -- batch window; advance each run
Verification: compare vertex counts with ST_NPoints before and after — near-duplicate vertices disappear, so the count drops slightly. Area should be essentially unchanged; a large area change means the grid is too coarse.
Step 3 — Align shared boundaries with ST_Snap
Snap each feature to the merged geometry of its neighbours so shared edges pick up identical vertices.
-- Align each parcel to its neighbours within a 0.05 m tolerance.
-- ST_Snap moves the first geometry's vertices onto the reference.
WITH neighbours AS (
SELECT
a.id,
a.geom AS geom,
ST_Union(b.geom) AS ref
FROM parcels a
JOIN parcels b
ON a.id <> b.id
AND ST_DWithin(a.geom, b.geom, 0.05)
GROUP BY a.id, a.geom
)
UPDATE parcels p
SET geom = ST_Snap(n.geom, n.ref, 0.05)
FROM neighbours n
WHERE p.id = n.id;
Verification: re-run the Step 1 gap query with a small ST_DWithin; the near-miss concentration should collapse toward zero as shared vertices now coincide exactly.
Step 4 — Re-node intersections with ST_Node
When you build or clean linework (road centrelines, parcel edges), ensure every crossing is an explicit shared node.
-- Merge all edges, then node them so every intersection becomes a vertex.
SELECT ST_Node(ST_UnaryUnion(ST_Collect(geom))) AS noded
FROM road_edges;
Verification: the result is a single MultiLineString in which no two segments cross without a shared endpoint. Feeding it to ST_Polygonize or a connectivity check should no longer report dangling or crossing edges.
Step 5 — Verify topology survived the snap
Snapping moves vertices, so confirm nothing broke.
-- Post-snap health check: invalid features and large area changes
SELECT
count(*) FILTER (WHERE NOT ST_IsValid(geom)) AS now_invalid,
count(*) FILTER (WHERE ST_IsEmpty(geom)) AS collapsed,
count(*) FILTER (
WHERE abs(ST_Area(geom) - area_before)
/ GREATEST(area_before, 1e-9) > 0.02
) AS big_area_change
FROM parcels_snap_audit; -- table holding pre-snap area_before per id
Verification: now_invalid, collapsed, and big_area_change should all be 0. Any non-zero count identifies features the snap damaged — repair them with ST_MakeValid or quarantine them.
Interpreting Results
The measured gap distribution from Step 1 is the whole ballgame. A tight concentration of near-misses well separated from your smallest feature dimension means snapping is safe and the tolerance choice is obvious. A gap distribution that overlaps the scale of real features is a warning: any tolerance large enough to close the gaps will also collapse genuine narrow geometry, and you should not snap the whole layer uniformly. In that case, snap selectively or escalate to a topology framework that reconciles the coverage as a whole.
Read the distribution as three bands. The first band, at or very near zero, is vertices that already coincide and need no snapping. The second band — the concentration of small non-zero gaps a little above zero — is the true near-miss population that a snap should close; the width of this band, not its centre, sets the minimum workable tolerance. The third band, at distances comparable to real feature sizes, is genuine separation between features that must never be snapped shut. A healthy dataset shows a clear empty valley between the second and third bands, and your tolerance belongs in that valley. When the valley is absent — the near-miss band bleeds continuously into the real-separation band — no single tolerance is safe, and uniform snapping will trade unaligned boundaries for collapsed features.
After snapping, the pair of numbers that matter are the residual near-miss count and the area delta. Residual gaps near zero with negligible area change is the success signal. Residual gaps that persist mean the tolerance was too small; a meaningful area change means it was too large or that a feature folded — both send the affected features to the same verification-and-repair path used across automated geometry remediation. Track the vertex-count delta as a third diagnostic: ST_SnapToGrid and ST_Snap both reduce vertex counts when they merge coincident points, so a modest drop is expected and healthy, while a sharp drop concentrated in a few features usually marks the ones where a ring partially collapsed.
Gotchas & Edge Cases
A tolerance larger than the smallest feature deletes that feature. Snapping is destructive when the tolerance exceeds a genuine dimension — a thin easement or a narrow parcel neck collapses to zero area. Always compare the chosen tolerance against the minimum real feature width before running a bulk snap.
ST_Snap is pairwise and order-dependent. Snapping A to B and separately B to A can yield different results, and snapping the whole layer to a single reference does not reconcile neighbours transitively. For coverage-wide alignment, snap each feature to the union of the features it touches, as Step 3 does.
ST_SnapToGrid can create invalid rings. Rounding coordinates to a coarse grid can make two distinct vertices coincide and fold a ring. Keep the grid size well below the feature scale, and follow with ST_IsValid.
Snapping does not enforce OGC topology rules. Making vertices coincide is necessary but not sufficient for a clean coverage; overlaps and gaps between features are a separate concern governed by understanding OGC topology rules. Snap first, then validate topology.
Un-noded snapped lines look connected but are not. Two lines can share a coordinate after snapping yet still lack a node at their crossing. Always run ST_Node before any network or polygon-building step.
When to Escalate
Snapping with these three functions handles local misalignment and precision cleanup. Escalate when:
- The layer must be reconciled as a single coverage. When gaps, overlaps, and shared boundaries must all be enforced consistently across every feature at once, move from ad-hoc snapping to the PostGIS Topology extension or a dedicated topology build, informed by understanding OGC topology rules.
- Snapping keeps producing invalid geometry. If the post-snap check in Step 5 flags many broken features, hand them to the ST_MakeValid repair procedure and reconsider the tolerance rather than snapping harder.
- The defect is single-feature invalidity, not misalignment. Self-intersections and unclosed rings are not a snapping problem; route them back through the automated geometry remediation classifier.
Related:
- Automated Geometry Remediation — the repair stage that orchestrates snapping alongside validity repair
- Repairing Invalid Geometries with ST_MakeValid — the sibling method for single-feature validity repair
- Understanding OGC Topology Rules — the coverage-level rules that snapping prepares data to satisfy
Back to Automated Geometry Remediation