bun-geos
On this page

Quickstart

The five-minute tour: create a context, build two geometries, overlay them, repair something, get out without leaking. Everything here is expanded in the guides — this page is the spine.

One context, own it explicitly

import { Geos } from "@shzd/bun-geos";

using geos = new Geos();

A Geos owns a GEOS context (and, on first construction in the process, loads the native libraries). using — TypeScript's explicit resource management — disposes it at scope exit. Creating it is cheap after the first time; the first call in a process pays the library load.

Build geometry from text, bytes, or JSON

// WKT — the universal debug format
const a = geos.fromWKT("POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0))");

// GeoJSON — the interchange format
const b = geos.fromGeoJSON({
  type: "Polygon",
  coordinates: [[[2, 2], [6, 2], [6, 6], [2, 6], [2, 2]]],
});

fromWKT returns a Geometry — an owned handle around a native geometry. WKB and GeoArrow round-trips work the same way (Geometry I/O has all four formats).

Overlay

using overlap = geos.intersection(a, b); // OverlayNGRobust under the hood
console.log(geos.toWKT(overlap));        // "POLYGON ((2 2, 4 2, 4 4, 2 4, 2 2))"
console.log(geos.area(overlap));         // 4

intersection, union, difference, symDifference all take two geometries and return a new one. The returned geometry is owned by you — hence the using. If you generate geometry in a loop, don't dispose one at a time; use an arena (Memory & disposal).

Repair the real world

Real-world polygons are invalid — self-intersecting rings, spikes, nested holes. GEOS 3.14's repair is one call:

const broken = geos.fromWKT("POLYGON ((0 0, 2 2, 2 0, 0 2, 0 0))"); // bow-tie
using fixed = geos.makeValid(broken);
console.log(geos.isValid(broken)); // false
console.log(geos.isValid(fixed));  // true

isValidDetail() hands you the reason and location of an invalidity as geometry, not prose. The full repair/validity story: Overlays & operations.

Ask spatial questions

console.log(geos.contains(a, geos.point(1, 1))); // true

using prep = a.prepare(); // prepared predicates: build once, ask many
console.log(prep.intersects(b)); // true

For "one geometry against ten thousand", skip the loop — the batch forms return typed arrays in one FFI crossing, and the STRtree indexes for envelope queries:

const tree = geos.strtree(geometries, 10);
const hits: Int32Array = tree.query(envelope); // candidate ids, one crossing

Predicates & indexing is the whole story.

Free it all

Dispose explicitly and scope-honestly:

{
  using geos = new Geos();
  using a = geos.fromWKT("POINT (0 0)");
  // …
} // a disposed, then geos (context + native library bookkeeping) — in order

Hot loops use geos.arena() so ten thousand temporaries die in one native call. A FinalizationRegistry will eventually reclaim anything you forget — that is the leak net, not a strategy. → Memory & disposal.

When things throw

  • fromWKT("POLYGON ((0 0)") → GeosError with the GEOS parser message.
  • geos.intersection(a, b) where b came from a different Geos → TypeError. Geometries are context-bound; that's a programming error, not a runtime failure.
  • Feeding lat/lon degrees to buffer(1000) → no error at all. Garbage answers, no exceptions. Project to a planar CRS first — see the warning on the front page.

The complete error taxonomy: Errors.

Where to next

Powered by Cirth