ADR 0007 — JSTS for topology and booleans; Turf only for cosmetics

Status: accepted · Amends: — · Amended by: —

Context

plugin-topology and plugin-edit need boolean operations and validity predicates: union (merge two parcels), difference and intersection (find an overlap), isSimple / isValid (reject a bowtie), noding, and precision reduction. The obvious JavaScript options are @turf/* (which delegates its booleans to polygon-clipping / martinez), and JSTS (a port of JTS, the Java Topology Suite, which is the engine behind PostGIS and GEOS).

Turf is smaller, tree-shakes properly, has a pleasant API and no ceremony. JSTS is a Java port with a Java shape: factories, geometry readers and writers, and deep-import paths that need their own .d.ts shims. On developer experience it is not close.

But this is the code that decides whether two parcels overlap, and the failure mode of the convenient option is not a crash.

Float-based clipping libraries produce artefacts on nearly-degenerate input: two parcels that share a boundary exactly — which is the normal case in a cadastre, because the shared edge was digitised once and reused — hit the exact case where a floating-point intersection test is most fragile. The union of two such parcels comes back with a spike; the difference comes back with a sliver of 10⁻⁹ m²; a self-touching ring comes back as valid. None of these throw. They render. And then a rule reports an "overlap" of 0.0000003 m² between two parcels that a surveyor knows perfectly well share a boundary, the surveyor learns that the overlap warnings are noise, and the day a real 3 m² overlap appears they dismiss it too.

A slightly wrong boolean result in a cadastre is worse than an exception, because an exception gets fixed.

Decision

JSTS for anything topological. Turf only for cosmetics and globe-scale approximations. Raw planar maths for survey-grade numbers.

Job Library
Boolean ops, validity, isSimple, noding, precision reduction JSTS
Area, length, offset, perpendicular foot — anything a surveyor signs planar maths in crs.working
Point-in-polygon hit tests, rough distance, bbox, centroid @turf/*
Nearest-feature and candidate prefetch rbush, via core's SpatialIndex

The decisive capability is JTS's precision model. JSTS can be told that coordinates live on a 1 mm grid, and then reason consistently on that grid:

// jsts.ts — JTS's PrecisionModel is a *scale*, not a grid size: 1 mm ⇒ scale 1000.
const model = new PrecisionModel(1 / precisionMetres)
return GeometryPrecisionReducer.reduce(geometry, model)

Everything entering an overlay operation is reduced to that grid first. Two corners 0.4 mm apart become the same corner, deliberately, before the boolean runs — so the operation cannot manufacture a sliver between them, because on its model they are not apart.

Both plugins run JSTS on geometry that has already been projected into the working CRS, in metres (ADR 0005). A precision model of "1 mm" is meaningless in degrees.

Neither library appears in the core. jsts is a dependency of plugin-topology and plugin-edit only — which is why preset-game, which installs neither, ships a bundle with no JSTS in it at all.

Alternatives rejected

@turf/union / @turf/difference (polygon-clipping). Rejected on the sliver argument above: floating-point clipping with no precision model manufactures spikes and slivers on exactly the input a cadastre produces most (shared edges). It is a fine library for cosmetic overlays, and we keep Turf for that.

martinez-polygon-clipping directly. Same class of problem, one dependency less.

WASM GEOS (geos-wasm and friends). The genuinely correct long-term answer: it is the same C++ engine PostGIS uses, with a real precision model and considerably better performance on large overlays. Rejected for now on three counts: the bundle is a large WASM blob to ship to a browser that may only ever draw one polygon; the async initialisation infects the plugin setup path; and the ergonomics of moving geometry across the WASM boundary need care to not be slower than JSTS for the small-n case, which is the common one. It is on the roadmap as an optional engine behind the same plugin API — the plugin owns the engine, so swapping it is a plugin-internal change, not a contract change. This ADR is what makes that swap cheap.

Rolling our own exact-predicate boolean layer (Shewchuk arithmetic + a scanline). Rejected without much agonising. Computational geometry is a field where the difference between "works on my test cases" and "correct" is measured in decades of bug reports, and JTS has had those decades.

Consequences

Edit this page on GitHub — this site is generated from docs/adr/0007-jsts-over-turf-for-topology.md, which is the source of truth.