kenro
SpatiaLite-style spatial SQL for SQLite, in pure Rust — PostGIS-compatible ST_ functions that work with rusqlite, as a loadable extension (Python / Node / Bun / Deno / Go / Ruby / C / sqlite3 CLI), in containers and serverless (Cloud Run / Lambda / Workers), and in the browser (sql.js / wa-sqlite / official SQLite WASM).
If you searched for rusqlite spatial, SQLite spatial functions without SpatiaLite, SpatiaLite alternative in Rust, or GeoPackage in pure Rust: this is that crate.
kenro is a full spatial SQL engine for SQLite: the PostGIS function surface — predicates, overlay, repair, buffering, reprojection, vector tiles, spatial aggregates, ~80 functions — in pure Rust, golden-tested against PostGIS itself, with zero C dependencies and one-call registration:
- Geometry I/O — WKT, WKB, GeoJSON and GeoPackage blobs in and out, MVT vector tiles out — all first-class citizens
- Predicates — the full DE-9IM family:
ST_Intersects/ST_Contains/ST_Within/ST_Touches/ST_Crosses/ST_Overlaps/ST_Equals/ST_Covers/ST_Relate, plusST_Distance/ST_DWithin(via georust/geo) - Overlay & repair (
fullfeature) —ST_Intersection/ST_Union(scalar and aggregate) /ST_Difference/ST_SymDifference/ST_Buffer/ST_MakeValidin pure Rust, with the differences vs GEOS quantified by golden tests - GeoPackage support — the exact function set the spec's R-tree (F.3) and geometry-type (F.4) maintenance triggers require
- CRS transform — pure-Rust proj4rs: WGS84, Web Mercator and every UTM zone built in, the full EPSG registry behind a feature flag, with measured accuracy
- H3 cells — mesh aggregation in
GROUP BY(h3-pg naming) - Vector tiles —
ST_AsMVTGeom+ theST_AsMVTaggregate with a hand-rolled, dependency-free encoder - Accessors, measures, processing — area, length, centroid, convex hull, line interpolation, simplification, affine transforms, …
The headline: with kenro registered, a plain SQLite build maintains a GeoPackage spatial index correctly. No SpatiaLite, no GDAL, no C toolchain.
kenro = 間縄 (kennawa) × rope — two words for the same cord, twisted into one. Named after the measuring rope of historical Japanese land surveys: the tool for turning land into ledgers.
Quickstart (Rust / rusqlite)
[]
= { = "0.1", = ["rusqlite"] }
let conn = open?;
register?;
// Query a GeoPackage with its R-tree index + a precise predicate refine:
let n: i64 = conn.query_row?;
Inserts, updates and deletes through SQL keep the spatial index in sync via the standard GeoPackage triggers — including files written by GDAL/QGIS.
Quickstart (every other platform — loadable extension)
kenro ships as a standard SQLite loadable extension — one prebuilt binary per OS on the releases page (Linux x86_64/arm64, macOS universal, Windows), loadable from any language whose SQLite driver exposes extension loading:
|
# → libkenro_ext.so (or build from source: cargo build -p kenro-ext --release)
=
Copy-paste guides for every platform live in docs/quickstart.md: Python, Node.js, Bun, Deno, Go, Ruby, C/C++, the sqlite3 CLI, containers (Cloud Run / Fly.io / ECS), AWS Lambda, and Cloudflare Workers — including cross-compilation and per-platform gotchas (macOS system SQLite, D1's no-UDF limitation, …).
Quickstart (browser — kenro-wasm)
Browser SQLite builds can't load native extensions, but they all accept JS-level user-defined functions — so kenro's SQLite-free core compiles to wasm — 412–946 KB (167–353 KB wire) depending on the feature tier (sizes) — with one adapter per host:
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
import initKenro * as kenroWasm from "kenro";
import from "kenro/sqlite-wasm";
await ;
const sqlite3 = await ;
const db = ;
;
db.; // POINT(1 2)
Adapters for sql.js and wa-sqlite ship alongside; host support matrix, measured sizes, and per-host limitations are in docs/wasm.md.
Live demo: https://reearth.github.io/kenro/ — drag a GeoPackage in
and query it with spatial SQL, entirely client-side (source in
crates/kenro-wasm/demo/).
Function reference
~80 SQL functions across geometry I/O (WKT/WKB/GeoJSON/GeoPackage), the
full DE-9IM predicate family (ST_Relate included), measures, overlay &
buffer, processing & affine transforms, accessors, constructors, GeoPackage
trigger helpers, H3, and MVT vector tiles — plus two aggregates
(ST_Union(geom), ST_AsMVT(…)).
The full table — every function with its PostGIS / DuckDB Spatial comparison and documented behavior differences — lives in docs/functions.md.
All implemented functions are deterministic and pure (no I/O, no clock,
no randomness) and NULL-strict (NULL in → NULL out; aggregates skip NULL
rows, following PostGIS aggregate semantics). Malformed input raises an
explicit error prefixed kenro: — never a silent NULL. Functions kenro
knows about but does not implement register as stubs whose error explains
what to use instead.
Semantics: PostGIS is the reference
Function names, signatures, and semantics follow PostGIS (SQL/MM ST_
prefix), validated against PostGIS-generated golden vectors committed in
this repo (tests/golden/*.jsonl — 700+ vectors across nine suites; H3
vectors come from the reference C library, MVT tiles are cross-decoded by
two independent decoders). Where kenro deviates, it does so loudly and
documentedly — never a silently different result. The full list of
divergences (empty-geometry handling, GeometryCollection, SRID rules, the
overlay engine's areal-only results, …) is in
docs/functions.md.
Choosing kenro vs PostGIS vs DuckDB Spatial
Structural differences that matter more than any single function:
- SRID model — PostGIS geometries and kenro's GeoPackage blobs both
carry their SRID; DuckDB's
GEOMETRYdoes not, so CRS bookkeeping is the user's job there (andalways_xyaxis-order care is needed for EPSG:4326). - Where it runs / weight — kenro lives inside SQLite: pure Rust, KB–MB scale, no C toolchain, deterministic. PostGIS is a server-side PostgreSQL extension. DuckDB spatial bundles GEOS + PROJ + GDAL (its WASM build is ~23.5 MB uncompressed, ~6.3 MB over the wire).
- Division of labor — kenro covers spatial SQL end-to-end inside your app's SQLite file: predicates, overlay/repair/buffer, R-tree maintenance, CRS transforms, MVT generation, aggregates. Reach for PostGIS or DuckDB spatial when you need what kenro deliberately leaves out — raster, topology/networks, file-format conversion, GeometryCollection-producing operations, datum-grid transforms. They compose rather than compete.
Supported CRS
ST_Transform ships a built-in EPSG table (proj4rs carries no EPSG
database). kenro is region-neutral: the built-in codes are exactly the
globally-defined, algorithmically-derivable systems —
| Codes | System |
|---|---|
| 4326 | WGS84 geographic |
| 3857 | Web Mercator |
| 32601–32660 | WGS84 UTM zones 1N–60N |
| 32701–32760 | WGS84 UTM zones 1S–60S |
Every national and regional system is served the same way: the crs-full
cargo feature adds the full crs-definitions registry as a fallback
(megabytes of tables, EPSG codes ≤ 65535 only); without it, an unknown code
raises an error naming the code and the feature. Accuracy against PROJ is
measured and documented in docs/accuracy.md — TL;DR:
nanometer-level projection math, but no datum grids: national datum
modernizations and earthquake-displacement models are not applied; use full
PROJ for survey-grade work.
Cargo features
The default (standard) set — transform (proj4rs), h3 (h3o),
geojson, mvt — covers most use cases: I/O, the whole predicate family,
GeoPackage triggers, measures/processing/affine, CRS transform, H3,
GeoJSON, and MVT vector tiles (tile clipping uses dedicated rectangle
algorithms, so MVT costs almost nothing).
full adds the one feature excluded from the default for size:
overlay (ST_Intersection/ST_Union/ST_Difference/ST_SymDifference/
ST_Buffer/ST_MakeValid — pulls the i_overlay mesh, the largest
single contributor to binary size). With overlay present, ST_AsMVTGeom
also upgrades to PostGIS-grade validity repair (invalid input and
snap-induced self-intersections are made valid before tiling). In wasm terms: standard 617 KB
(251 KB gzip) vs full 946 KB (353 KB gzip); --no-default-features
gives a 412 KB (167 KB gzip) minimal build.
Building with any feature off keeps the corresponding SQL functions
registered as stubs that explain which feature is missing. rusqlite (off
by default) enables kenro::register. The prebuilt loadable extension
(kenro-ext) always ships full.
Requirements & caveats
- SQLite ≥ 3.31 (for
SQLITE_INNOCUOUS, which lets kenro functions run inside the GeoPackage R-tree triggers underPRAGMA trusted_schema=off); the loadable-extension path needs SQLite ≥ 3.34 (it fails with a clear version-mismatch message on older hosts). - Loading kenro and SpatiaLite into the same connection: both register
ST_names and SQLite keeps the last registration. Don't mix them (a registration-filter feature flag can be added if needed). ST_Distanceis 2D cartesian in the geometry's coordinate system. For meters over lon/lat, reproject to the local UTM zone first (e.g.ST_Transform(geom, 32654)around Tokyo,32633around Berlin).
Roadmap
- ✅ Core: GeoPackage blobs, WKB/WKT, predicates, R-tree functions, rusqlite registration, PostGIS golden tests
- ✅
ST_Transform(proj4rs; accuracy measured and documented), H3 cell IDs, GeoJSON, accessors - ✅
kenro-ext: loadable extension (.so/.dylib/.dll) for Python / Node / sqlite3 CLI - ✅
kenro-wasm: browser builds (official SQLite WASM / sql.js / wa-sqlite, details) + drag-and-drop GeoPackage demo - ✅ v0.3: full predicate family +
ST_Relate, measures/processing/affine, pure-Rust overlay &ST_Buffer, SQL aggregates (ST_Union), MVT (ST_AsMVTGeom+ST_AsMVT),GPKG_IsAssignable - ✅ Release pipeline: prebuilt extension binaries (Linux x86_64/arm64, macOS universal, Windows) + wasm bundle on every
v*tag - ✅ Public repository + live demo on GitHub Pages
- v0.x releases on crates.io (+ npm as
kenro)
License
MIT OR Apache-2.0, at your option.