Predicates & indexing
Spatial questions, three speeds: single predicates, prepared predicates, and indexed or batch forms. The rule of thumb: if you're writing a JS loop over a spatial question, you're using the wrong API — there is a batch form that does it in one FFI crossing, or an index that skips most of the work entirely.
The predicates
geos.intersects(a, b); geos.disjoint(a, b);
geos.touches(a, b); geos.crosses(a, b);
geos.within(a, b); geos.contains(a, b);
geos.overlaps(a, b); geos.covers(a, b); geos.coveredBy(a, b);
Two flavors of equality:
geos.equals(a, b); // topological equality (POINT (0 0) == MULTIPOINT((0 0)))
geos.equalsIdentical(a, b); // structural identity — same coordinates, same structure
And the full DE-9IM, when you need the raw relation:
const matrix = geos.relateMatrix(a, b); // "212101212" — the DE-9IM string
const ok = geos.relatesPattern(a, b, "T*T***T**"); // query by mask
Prepared predicates — build once, ask many
If one geometry participates in many tests, prepare it. Preparation builds an internal structure once; each subsequent predicate skips the repeated work. On scan-heavy workloads this is the difference measured in the repo's benchmark suite at ~32× over unprepared scans.
using prep = a.prepare();
prep.intersects(b); prep.contains(b); prep.covers(b);
prep.coveredBy(b); prep.crosses(b); prep.disjoint(b);
prep.overlaps(b); prep.touches(b); prep.within(b);
prep.containsProperly(b);
// point tests without building a point geometry:
prep.intersectsXY(x, y); prep.containsXY(x, y);
// the full DE-9IM, prepared:
prep.relate(b); // matrix string
prep.relatePattern(b, "T*T***T**");
Prepared is disposable — using it like any geometry. It holds a reference to its
source geometry; disposing the source before the prepared structure is a programming
error and throws.
Batch predicates — one crossing, a typed-array answer
One geometry against ten thousand: the *All forms run the whole batch in one
shim crossing and return a Uint8Array mask (1/0 per input) — no JS loop over FFI
calls, no per-item allocation:
const hits: Uint8Array = geos.intersectsAll(a, candidates);
const inside: Uint8Array = geos.containsAll(a, candidates);
// every predicate has a batch form, including via name:
const m: Uint8Array = geos.predicateAll("within", a, candidates);
Also batched (same BatchInput, typed-array or Geometry[] answers):
coveredByAll, coversAll, crossesAll, disjointAll, overlapsAll,
touchesAll, withinAll — and the measure/geometry forms
(areaAll, lengthAll, distanceAll, bufferAll, centroidAll, simplifyAll,
clipByRectAll, nodeAll, lineMergeAll, snapAll, …). The full list:
API reference.
Note
*All masks are aligned to the input order. hits[i] === 1 means
candidates[i] answered yes — there is no result reordering anywhere.
STRtree — the spatial index
Envelope-index many geometries, query by envelope, get ids back:
const tree = geos.strtree(geometries, 10); // nodeCapacity 10 is the classic default
const hits: Int32Array = tree.query(envelope); // candidate ids, ONE crossing
const id = tree.nearest(queryGeom); // -1 for an empty tree
const g = tree.get(id); // the inserted geometry back
- Candidates, not answers:
queryreturns envelope hits. The exact filter is your job — typically a prepared predicate on the returned ids. That split (index narrows, predicate decides) is the standard GEOS/PostGIS pattern and it is why the query is one crossing. querybuilds the tree on first use;build()is there if you want to pay the cost explicitly. After build, the tree is frozen —inserton a built tree throws.queryFilter(env, prepared, pred)pushes the exact predicate into the C callback: one crossing that returns only the ids that pass, when you'd otherwise loop in JS.nearestwalks a C distance table — correct nearest-neighbor, no JS loop.
The index measured ~122× over a naive JS scan in the repo's benchmark suite; the point isn't the number, it's that the crossing count stops mattering.
Choosing the right speed
| Situation | Use |
|---|---|
| one question, two geometries | plain predicate |
| one geometry, many questions | prepare() |
| one question, many geometries | *All batch form |
| many questions, huge set, envelope-shaped | STRtree + queryFilter |
| the batch doesn't fit one core | geos.parallel.* → Workers & parallelism |
Where to next
- Workers & parallelism — when one core isn't enough
- Memory & disposal —
PreparedandSTRtreeown native resources too - API reference — the full predicate/batch surface