Check whether a geographic coordinate is on water. Exposed via an HTTP API for single coordinate (GET /api/water?lat=${lat}&lon=${lon}) and batch (POST /api/water with { "coordinates": [{ lat, lon }, ...] }) lookups.
Coverage is opt-in per deployment. Out of the box you get oceans, seas, and inland lakes ≥ 2 km²; enable rivers and ponds for OSM riverbanks and smaller inland water. See Coverage layers.
Built on Fastify with optional OpenTelemetry, Swagger at /documentation, and rate limiting (in-memory by default; Redis when REDIS_URL is set).
git clone https://github.com/osbytes/is-on-water
cd is-on-water
pnpm install
## run in development with hot-reload
pnpm dev
## OR build and run
pnpm build && pnpm startSee .env-example for all environment variables.
Rate limiting uses an in-memory store by default (no Redis required).
To share limits across multiple instances, set REDIS_URL (for example redis://localhost:6379). You can start Redis with:
docker compose --profile redis upWhen REDIS_URL is set, /health also pings Redis and returns 503 if it is unreachable.
Disabled by default. Enable with OTEL_ENABLED=true.
By default the trace exporter writes to standard output. Set OTEL_EXPORTER_OTLP_ENDPOINT (for example http://localhost:4318/v1/traces) to export to a collector. docker compose up starts Jaeger; UI at http://localhost:16686.
Interactive docs: /documentation
Errors use RFC 9457 Problem Details (application/problem+json).
curl "http://localhost:3000/api/water?lat=20.112682&lon=-37.048647"{ "water": true, "lat": 20.112682, "lon": -37.048647, "layer": "oceans:medium" }layer names the enabled layer that matched, or is null when the coordinate is not on water in any enabled layer. Because coverage is configurable, "water": false means "not found in the layers this instance has enabled" rather than a guarantee of dry land.
Latitude must be between -90 and 90; longitude between -180 and 180.
Body: { "coordinates": [{ "lat", "lon" }, ...] } (max MAX_BATCH_SIZE, default 500). A bare JSON array is also accepted.
curl -X POST http://localhost:3000/api/water \
-H 'content-type: application/json' \
-d '{"coordinates":[{"lat":20.112682,"lon":-37.048647},{"lat":40.292097,"lon":-98.613164}]}'{
"results": [
{ "water": true, "lat": 20.112682, "lon": -37.048647, "layer": "oceans:medium" },
{ "water": false, "lat": 40.292097, "lon": -98.613164, "layer": null }
]
}Reports which layers this instance has enabled, so a client can interpret a false result.
{
"layers": [
{
"id": "oceans:medium",
"feature": "oceans",
"precision": "medium",
"delivery": "bundled",
"scope": "Oceans and seas; ~333 m shoreline detail",
"license": "ODbL",
"attribution": "© OpenStreetMap contributors"
}
]
}Coverage is addressed as {feature}:{precision} pairs. A feature is a kind of water; a precision controls how much geometric detail the artifact keeps and, for lakes, how small a water body has to be before it is dropped. Higher precision therefore means both finer shorelines and more water bodies — at the cost of a larger artifact.
| Feature | Source | License |
|---|---|---|
oceans |
OSM coastline water polygons | ODbL |
lakes |
HydroLAKES v1.0 | CC-BY 4.0 |
rivers |
OSM river/canal area geometries via Geofabrik continent extracts | ODbL |
ponds |
OSM ponds/small lakes/reservoirs (≤ 2 km²) via Geofabrik continent extracts | ODbL |
| Precision | Simplify tolerance | Lakes min area | Ponds area window |
|---|---|---|---|
low |
0.01° (~1.1 km) | 10 km² | 0.1–2 km² |
medium |
0.003° (~330 m) | 2 km² | 0.01–2 km² |
high |
0.0008° (~89 m) | 0.5 km² | 0.001–2 km² |
full |
none | 0.1 km² | ≤ 2 km² |
rivers has no area filter — precision only changes shoreline simplification. Stream centerlines are out of scope at every precision; only mapped riverbank / river / canal polygons count.
Select layers with WATER_LAYERS:
WATER_LAYERS=oceans:medium,lakes:medium # default
WATER_LAYERS=oceans,lakes,rivers,ponds # max default-precision coverage
WATER_LAYERS=oceans:full,lakes:high,rivers:high,ponds:high
WATER_LAYERS=all # every feature at its default precisionA bare feature name uses that feature's default precision, and a feature listed more than once collapses to the highest precision requested, so oceans:low,oceans:full resolves to oceans:full.
oceans:medium and lakes:medium ship in this repository. Global rivers:medium (~138 MB) and ponds:medium (~820 MB) are published as GitHub Release assets (delivery: download) and fetched once at boot into WATER_LAYER_CACHE_DIR (default data/_layer-cache), where they are verified against the checksum in the registry and reused across restarts. Higher-precision combinations use the same release channel.
To host artifacts yourself, or to add features this project does not ship, point WATER_LAYERS_REGISTRY at your own registry file modelled on data/layers.json. Each artifact declares how it is delivered:
bundled— read from the data directorydownload— fetched once at boot, checksum-verified, cached on diskrange— never downloaded; queried in place over HTTP range requests, which keeps memory near zero at the cost of network latency per query
Each layer is a gzip-compressed FlatGeobuf file under data/layers/, catalogued in data/layers.json with its source, license, checksum, and size.
Lookups query the FlatGeobuf packed Hilbert R-tree directly with a point-sized bounding box, so only the handful of polygons whose envelope contains the coordinate are ever parsed. Nothing is re-indexed in memory at startup.
Treat shoreline results as approximate: every bundled layer is Douglas–Peucker simplified, and river centerlines are out of scope at any precision.
Rebuild locally (requires Docker with a GEOS-enabled GDAL image, or USE_HOST_OGR=1):
pnpm dataset:build # oceans + lakes (bundled)
pnpm dataset:build:inland # rivers + ponds from Geofabrik PBFs
BUILD_LAYERS=oceans:full,lakes:high pnpm dataset:build
GEOFABRIK_REGIONS=europe,africa pnpm dataset:build:inland # partial rebuildBUILD_LAYERS chooses what to build and BUNDLED_LAYERS chooses which of those are committed rather than published as release assets. The inland builder caches Geofabrik continent PBFs under data/_osm_inland/ (tens of GB) and merges them; set GEOFABRIK_REGIONS to limit which continents are included. Artifacts built by earlier runs are preserved in the registry, so building one precision does not drop the others.
GET /api/nearest?lat=&lon= returns nearby water polygons from the enabled layers as a nearest array ordered by ascending distance, then descending area. Each hit is the nearest shoreline point on that body (not necessarily a coordinate that /api/water would mark water: true, since ring boundaries are outside the polygon fill).
| Param | Default | Description |
|---|---|---|
count |
5 |
Max hits to return (1–25) |
type |
all enabled | Comma-separated features: oceans, lakes, rivers, ponds |
maxKm |
100 |
Search radius cap (0.1–500) |
A monthly GitHub Action checks the OSM zip’s Last-Modified / ETag, rebuilds data/, runs the dataset validation suite, and opens a PR when something changed.
© OpenStreetMap contributors (ODbL). HydroLAKES © HydroSHEDS / Messager et al. (CC-BY 4.0).
This repo uses Changesets.
- During development, record user-facing changes:
pnpm changeset - On push to
main, GitHub Actions opens (or updates) a Version Packages PR - Merging that PR bumps
package.json, updatesCHANGELOG.md, tags the release, and creates a GitHub Release
CI (typecheck, test, build) runs on every pull request and push to main.
MIT — see LICENSE. Map data remains under OSM/ODbL as noted above.