cairn.
v0.1.0 · beta ★ GitHub

Geocoding,
without the stack.

Cairn turns OpenStreetMap, WhosOnFirst, OpenAddresses, and Geonames data into a single on-disk bundle, then serves forward search, autocomplete, fuzzy matching, structured queries, and reverse geocoding from one static Rust binary. No cluster. No cloud. No daemon dependencies.

0.74 ms
p99 latency
57.5k RPS
peak throughput
80 MB
resident memory
24 s
build wall-clock
CH bundle, 6 408 queries · vs Pelias on the same input: 77× faster p99, 159× higher RPS, 34× smaller RSS. How →
Cairn mark
v0.1.0 · beta · MIT
est. 2026
02 — Quickstart

Stack your first cairn in 30 seconds.

Build a country bundle from one OSM PBF, serve it, query it. No Postgres, no Elasticsearch, no Java. Pure Rust + mmap'd rkyv tile blobs.

build & serveshell
# 1. build a Liechtenstein bundle
$ cairn-build build \
    --osm liechtenstein-latest.osm.pbf \
    --wof whosonfirst-data-admin-li.db \
    --out bundle

# 2. serve it (single static binary)
$ cairn-serve --bundle bundle

# 3. query it
$ curl 'localhost:8080/v1/search?q=Vaduz'
responsejson
{
  "query": "Vaduz",
  "results": [
    {
      "name":  "Vaduz",
      "kind":  "city",
      "lon":   9.522,
      "lat":   47.139,
      "label": "Vaduz, Schweiz",
      "langs": ["de", "en", "fr"],
      "score": 48.45
    }
  ]
}
03 — Deploy

Production-ready, on the platform you already use.

Cloud deployment artifacts live in cairn-geocoder/cairn-cloud. Helm chart published to OCI; Kustomize base + dev/prod overlays; Terraform modules for AWS Fargate, GCP Cloud Run, and Nomad + Consul; Grafana dashboard + Prometheus alerting rules wired to Cairn's /metrics output.

04 — Core concepts

How Cairn thinks.

Six ideas that explain the design. Read these once and the rest of the docs makes sense.

05 — What's new in v0.3 / v0.4

Enrichers + stable identifiers.

Two recent landings layer on top of the v0.2 build pipeline: augmenters that enrich an existing bundle in place (no rebuild needed), and a stable global identifier (gid) on every Place so bookmarks survive rebuilds and federation hops.

06 — Examples

Recipes from the trail.

Short, complete curl snippets covering the most common queries against a running cairn-serve.

i.

Forward search

$ curl 'localhost:8080/v1/search?q=Vaduz&limit=2'
{
  "results": [
    { "place_id": 1099603968001,
      "gid":      "wof:locality:101748479",
      "name": "Vaduz", "kind": "city",
      "lon": 9.522, "lat": 47.139 }
  ]
}
# place_id is bundle-local (rebuilds change it).
# gid is rebuild-stable: bookmark it, resolve via /v1/place.
ii.

Lookup by gid

# Lookup by stable gid. Mix gids and u64 place_ids freely;
# missing identifiers return empty (not 404).
$ curl 'localhost:8080/v1/place \
    ?ids=osm:way:12345,wof:locality:101748479'
iii.

Autocomplete

# Prefix-ngram index. Cheap enough for keystroke-grain UI.
$ curl 'localhost:8080/v1/search?q=Vad&mode=autocomplete'
iv.

Fuzzy + phonetic

# Edit distance + DoubleMetaphone
$ curl 'localhost:8080/v1/search?q=vaaduz&fuzzy=2'
$ curl 'localhost:8080/v1/search?q=Smyth&phonetic=true'
# Phonetic rescues 99.5% of typos in Cairn's noisy benchmark.
v.

Layer + focus bias

$ curl 'localhost:8080/v1/search?q=Vaduz \
    &layer=city \
    &focus.lat=47.165&focus.lon=9.51 \
    &focus.weight=2.0'
vi.

Structured search

$ curl 'localhost:8080/v1/structured \
    ?road=Aeulestrasse&city=Vaduz'
vii.

Reverse geocoding

$ curl 'localhost:8080/v1/reverse \
    ?lat=47.141&lon=9.523&limit=4'
{
  "source": "pip",
  "results": [
    { "name": "Vaduz",    "level": 1 },
    { "name": "Oberland", "level": 1 },
    { "name": "Liechtenstein", "level": 0 }
  ]
}
07 — Reference

The HTTP API at a glance.

Each endpoint shows a one-line plain description plus the technical knobs available. Substitute localhost:8080 for whatever cairn-serve binds.

MethodEndpointDescriptionSince
GET /v1/search "What place am I looking for?" — type a name or address, get a ranked list back. Forward + autocomplete + fuzzy + phonetic + semantic + layer + focus + bbox. v0.0.1
GET /v1/structured "I have address fields, find matches" — pass street, city, postcode separately for tighter ranking. Field-by-field address search with auto layer hint. v0.0.1
GET /v1/reverse "What is at this point on the map?" — give it lat/lon, get the city / district / country containing it (with the closest POI as a fallback). Point-in-polygon with nearest-centroid fallback. v0.0.1
GET /v1/parse "Split this messy address into parts" — input one string, get back house number / street / city / postcode separately. Free-text address → ParsedAddress (heuristic + libpostal). v0.0.1
GET /v1/expand "Give me alternate spellings" — turn one address into the normalized variants ("St" ↔ "Saint", abbreviations expanded) for better recall. Free-text address → normalized variants. v0.0.1
GET /v1/place "I saved this place yesterday — find it again" — pass a stable identifier (or several) and get the full record. Bookmark-friendly: identifiers survive bundle rebuilds. Lookup by stable gid (osm:way:12345) or legacy bundle-local u64. Comma-separated list. v0.4 · UPDATED
GET /v1/buildings "Which building is at this point?" — returns the actual building footprint(s) covering a coordinate (rooftop-level, not just enclosing rectangle), or the closest N if you ask for nearest mode. Building footprints at lon/lat. ?mode=at with strict ray-cast PIP (default) or ?mode=nearest. Pass ?strict=false for the bbox-only fast path. v0.3 · NEW
GET /v1/info "What's this server running?" — version, bundle ID, uptime, federation members. Useful for debugging and dashboards. Bundle metadata, federation IDs, uptime. v0.1.0
GET /v1/sbom "Show me the bill of materials" — every dependency, every dataset, every version. For audit, supply-chain scanning, vulnerability triage. CycloneDX 1.5 SBOM for the running bundle. v0.1.0
GET /healthz · /readyz · /metrics Liveness · readiness · Prometheus exposition. v0.0.1
08 — Bundle

What's in a bundle.

One directory. Everything Cairn needs at runtime, anchored by blake3 hashes in manifest.toml. cairn-build verify recomputes them and refuses any drifted artifact.

bundle/tree
bundle/
├── manifest.toml              schema, source hashes, blake3
├── manifest.toml.sig          ed25519 detached signature (optional)
├── sbom.json                  CycloneDX 1.5 software + data BoM
├── tiles/<level>/<row>/<col>/<id>.bin   rkyv-archived Place blobs
├── index/text/                tantivy segments (mmap'd at runtime)
└── spatial/
    ├── admin/<level>/<tile>.bin   per-tile rkyv AdminLayer
    └── points/<tile>.bin          per-tile bincode PointLayer
09 — Build sources

Bring your own data.

Mix and match. OSM alone is enough for a working bundle; the rest fill specific holes.

SourceFormatFlagCoverage
OpenStreetMap*.osm.pbf--osmPlaces, POIs, streets, admin polygons.
WhosOnFirstSQLite per-country--wofHigh-quality admin polygons + multilingual names.
OpenAddressesCSV per-region--oaAuthoritative addresses where OSM is sparse.
GeonamesTSV--geonames · --postcodesPopulated places + postal codes.
10 — Benchmark

Switzerland on a Mac.

Same 506 MB OSM PBF, same 6 408-query workload, same Apple Silicon host. Cairn vs Pelias, Nominatim, Photon. Reproducible — benchmarks/ ships every script.

EngineBuildDiskHot RSSp50p99Peak RPS
Cairn 24 s 195 MB 80 MB 0.51 ms 0.74 ms 57 554
Pelias3 m 46 s3.5 GB2.7 GB 13.76 ms57.23 ms362
Nominatim3 h 13 m9.2 GB2.4 GB 9.51 ms23.00 ms1 109
Photon2 m 1 s1.3 GB2.1 GB 5.88 ms25.18 ms2 406

Cairn is 31–77× faster at p99, sustains 24–159× higher peak RPS, with 26–34× smaller hot RSS and 6.7–48× smaller disk than each incumbent. Build wall-clock is 5–483× faster. Recall on 1 153 noisy queries: ?phonetic=true lifts hit-rate from 21.9% to 99.0% alone.

Country-scale check — Germany

Same pipeline, 8.6× the input (4.7 GB PBF, ~3 M places vs 506 MB / 520 k for Switzerland). Steady-state numbers on the same Apple Silicon host:

MetricSwitzerlandGermanyRatio
Input PBF506 MB4.7 GB8.6×
Build wall-clock20 s487 s24×
Bundle disk212 MB1.54 GB7.4×
Hot serve RSS102 MB359 MB3.5×
p50 latency0.68 ms0.57 ms0.84×
p99 latency1.32 ms2.35 ms1.8×
Peak RPS23 47739 664 (c=32)1.7×

Steady-state hot RSS scales sublinearly (3.5× for 8.6× input) — rkyv tiles stay mmap'd; resident set is what's actively touched. p99 stays under 2.4 ms at 3 M places. Peak RPS at c=32 actually exceeds Switzerland's single-bundle peak — concurrent hot paths scale with rayon worker count. Post Phase 6f / 6g + parallel admin assembly + tantivy buffer tuning, DE peak RPS lifted from 8.4 k → 39.7 k and p99 dropped from 3.71 ms → 2.35 ms on the same Apple-Silicon host.

x86_64 reproduction: a manual GitHub Actions bench workflow runs the same harness on ubuntu-latest (Intel) so numbers aren't arm64-specific.

Germany — head-to-head with the incumbents

Same 4.7 GB Geofabrik PBF, same Mac, same 10 000-query DE workload. Cairn vs Pelias vs Photon at country scale:

EngineBuildDiskHot RSSp50p99RPS peak
Cairn8 m 7 s1.54 GB359 MB0.57 ms2.35 ms39 664
Pelias37 m5.28 GB5.15 GB10.29 ms23.16 ms506
Photon19 m 22 s9.10 GB2.13 GB9.69 ms92.94 ms1 919
Nominatimnot run — ~28 h projected on arm64 single-thread osm2pgsql

On 3 M places, Cairn keeps a 10–40× p99 lead, 21–78× peak RPS, and 6–14× smaller hot RSS than each incumbent. Build is 2.4–4.6× faster in wall-clock and produces a 3.4–5.9× smaller on-disk artifact.

11 — Community

Stack with us.

Cairn is built in the open. Issues, PRs, and design discussions all happen on GitHub.

Latest milestone

v0.1.0 — "Germany"

First public beta. Pelias drop-in parity (Tier 1) + quality lead (Tier 2) + ops polish (Tier 3) + differentiators (Tier 4) all shipped. Reproducible Switzerland + Germany benchmarks land Cairn 6–43× faster at p99 than every incumbent on the same input. Helm chart + Terraform modules ship in cairn-cloud.

2026 · beta
GitHub
★ Issues + PRs

11-crate Rust workspace. Dual MIT / Apache-2.0. CI on every push.

github.com/cairn-geocoder/cairn →
Live demo
cairn.kaldera.dev

9 preset queries plus a free-form composer dispatched live to a hosted Switzerland bundle.

Open the demo →
11 — Security

Live CVE pipeline.

The production container image is scanned by Trivy on every push, every PR, and once a day at 06:17 UTC. The table below mirrors the most recent scan; the canonical source is the GitHub Code Scanning tab. Reporting policy and threat model live in SECURITY.md.

Loading current CVE list…
CVE Severity Package Installed Fixed in Title
Loading…

Triage policy: CRITICAL ≤ 7 days, HIGH next release, MEDIUM routine maintenance. Anything not listed here is either patched, ignored with documented reason, or below the MEDIUM reporting threshold.