landcheck: is this point on land or in the sea?
An offline lookup library built on the Trifold grid. Thanks to exact aperture-4 nesting, the level-10 grid (~7 km cells) classified against Natural Earth collapses into a 182 KB dataset that answers anywhere on Earth in microseconds, with a confidence value for every answer. Python and JavaScript give identical results. This page runs the real JS library in your browser; the dataset is embedded right in this HTML file.
Try it on the mapInteractive demo
Load sample points or your own file (CSV lon,lat or GeoJSON points), and
every point is classified in your browser by the bundled library, with no server
and no network call per lookup. The lookups-per-second figure is measured tightly around
the classification loop on your machine (map rendering and file parsing excluded),
so it is the real library throughput. Use the 100k-random button for a stable number.
Switch to Route mode to classify a polyline — draw one on the map or pick an
example — and see the land/coast/sea stretches it crosses, each with its distance.
Controls
Points: classify many lon/lat points. Route: classify a polyline and see which land/sea stretches it crosses.
Pick an example, or hit Draw on map and click to drop points; click Finish (or the button again) to classify.
CSV: lon,lat[,name] per line (or a header naming
lat/lon columns in either order). GeoJSON: a
FeatureCollection of Points (Points mode) or a LineString (Route mode).
Files stay on your machine.
Off: coastal answers use the bundled land-area fraction. On: near-exact coastline. Watch how the counts, confidence and lookup rate change.
Click anywhere on the map to see the level-10 triangle and its classification.
land: certain (confidence 1.0)
coast: mixed cell
sea: certain (confidence 1.0)
answer flipped by OSM refinement
Loading dataset…
Click any classified point for its full answer: cell address (computed on the fly for sea points, whose cells are not stored), kind, confidence and land fraction. Note the Natural Earth 1:50m caveats: lakes count as land and islets below its resolution are missing. Switching on the OSM coastal refinement makes OSM authoritative in cells crossed by either source coastline. Try the cities sample with it on and off and compare the answers near coasts.
User guide
JavaScript (browser or Node)
import { LandCheck } from "./landcheck.mjs";
// Node: bundled file · browser: fetch the 182 KB dataset
const lc = await LandCheck.fromFile(); // Node
const lc = await LandCheck.fromUrl("landsea_L10.tfls"); // browser
lc.isLand(24.7536, 59.437); // true (lon, lat)
lc.check(-0.1276, 51.5072);
// { land: true, kind: 'land', confidence: 1,
// landFraction: 1, cell: 'TFA95BM', refined: false }
// a whole route: land/sea segments with distances
lc.checkPolyline([[24.75,59.44],[18.07,59.33]]).segments;
// [{land:true,kind:'land',...,distanceKm,fraction}, ...]
Python (stdlib only)
from landcheck import LandCheck
lc = LandCheck() # bundled data
lc.is_land(24.7536, 59.4370) # True
lc.check(-0.1276, 51.5072)
# LandResult(land=True, kind='land', confidence=1.0,
# land_fraction=1.0, cell='TFA95BM', refined=False)
# vectorised: ~2.8 µs/point with numpy
lc.is_land_batch(lons, lats)
# a whole route: land/sea segments with distances
lc.check_polyline([(24.75, 59.44), (18.07, 59.33)]).segments
What the answer means
| kind | meaning | land | confidence |
|---|---|---|---|
| land | cell wholly inside land | true | 1.0 |
| sea | cell absent from the dataset | false | 1.0 |
| coast | mixed cell; bundled land-area fraction decides | fraction ≥ 0.5 | max(f, 1−f) |
| coast + refined | decided by the optional OSM polygon layer | exact | 0.99 |
Measured accuracy: 99.82% agreement with exact polygon containment on
30,000 uniform random points. The land and sea answers were 100%
correct; all residual error lives in coast answers, which self-report lower
confidence.
With the OSM refinement loaded, coastal answers reach 99.95%.
Command line
$ python landcheck/python/landcheck.py 24.7536 59.4370
LAND kind=land confidence=1.000 land_fraction=1.0 cell=TFAVKGR refined=False
Technical info
Canonical index
Any Trifold cell at level ≤ 10 maps to
addr64 >> 39, a 25-bit integer where a level-l cell covers exactly
410−l consecutive indices. The whole classification becomes run-length
intervals.
TFLS format · 182 KB
153,884 runs as
varint(gap), varint(len·2|coastal) + a 4-bit land fraction per coastal
cell, zlib-compressed. Level-agnostic: the same tooling serves an L8 (~30 KB) or
L12 (~3 MB) variant.
Lookup path
Pure-float point location (no dependencies, bit-identical to the SDK) descends 10 subdivision levels, then one binary search over the run starts. ~0.8 µs in Node, ~13 µs in pure Python, ~2.8 µs batched with numpy.
OSM refinement · TFLR
OSM simplified land polygons clipped to every cell crossed by either source coastline, quantized to a cell-local 16-bit grid (~0.1 m), with zigzag-varint rings and the even-odd rule. The OSM polygon test can override Natural Earth land, sea or fraction answers in those cells.
Full documentation, build scripts (build.py,
refine_build.py) and the cross-language test suite live in
landcheck/ on GitHub.
Roadmap: country detection with the same run-length + clipped-border approach, an L12
variant, published pip/npm packages.
Benchmark: Trifold vs SQL spatial engines
Same job for every engine: classify 100,000 sphere-uniform random points against the same OSM simplified land polygons. Median of seven warm runs on an Apple M5 Pro laptop (June 2026); BigQuery ran as a managed on-demand service. In batch mode the OSM-refined Trifold was 3–4× faster than BigQuery and PostGIS and ~30× faster than DuckDB Spatial; called one point at a time it answered ~86,000 lookups per second, 40× the embedded DuckDB rate.
Batch · 100,000 points per call
Trifold base
459,096 pts/s
Trifold + OSM refinement
435,463
Singular · one point per call
Trifold + OSM refinement
85,666
Route (polyline) · per sampled point
Trifold base
38,016 samples/s
Trifold + OSM refinement
37,231
The SQL engines compute exact polygon containment on the loaded OSM snapshot;
Trifold's compact dataset agrees with that result on 99.5% of points (refined). PostGIS singular
includes localhost TCP + Docker transport; DuckDB runs embedded in-process. The
Route (polyline) row is per sampled point — a line query is one point-in-polygon per
sample for every engine; the live Route mode above reports the same per-sample rate on your
device. These bars are generated from the benchmark doc at build time. Full methodology,
dataset manifest and caveats:
benchmark.md.