Geomilli: Self-Hosted OpenStreetMap Geospatial Search API with Meilisearch

TL;DR: Quick Summary

Geomilli is an open source Apache-2.0 geospatial platform that converts raw OpenStreetMap extracts into a fast self-hosted place search API. A streaming harvester daemon downloads country PBF files, filters them with Osmium, maps features into 19 canonical classes, enriches them with Wikidata and Wikipedia pageviews, and indexes them into Meilisearch with zero-downtime swaps. A stateless Go API then serves sub-10ms autocomplete, full-text faceted search, reverse geocoding, nearby radius search, bounding-box queries and IATA/ICAO airport lookup. A React MapLibre console called Geomilli Studio provides an API playground, importer control with live progress, and latency telemetry. Default ingest is Belgium in about 60 to 90 seconds via Docker Compose. For amateur radio, it’s a self-hosted gazetteer for SOTA and POTA planning, APRS map labels, repeater and club finders, DXpedition airport routing, and offline EmComm dashboards with no Google API key or usage bill.


What Is Geomilli?

Geomilli is a self-hosted open source geospatial search platform that ingests OpenStreetMap data and serves it through a high-performance Go REST API backed by Meilisearch with sub-10ms autocomplete and multilingual place search.

The repository at https://github.com/obaid22192/geomilli is a monorepo with four parts: geomilli-api (stateless Go REST service), geomilli-harvester (streaming OSM ingestion daemon), geomilli-studio (React 19 plus TypeScript plus MapLibre developer console), and docs (VitePress documentation site published to GitHub Pages). Full docs live at https://obaid22192.github.io/geomilli/. The stack needs Go 1.25 plus, Meilisearch v1.13, Docker Compose v2, and osmium-tool inside the harvester image.

Geomilli Studio map explorer showing an interactive MapLibre map with API playground for autocomplete search reverse and nearby queries
Geomilli Studio map explorer and API playground with live MapLibre map, endpoint tester and one-click cURL generator. Image credit: obaid22192/geomilli repository, Apache-2.0.

Architecture at a Glance

Component Responsibility Tech stack Scaling note
geomilli-api Autocomplete, search, reverse, nearby, bbox, airports, telemetry Go 1.25, Chi router, Meilisearch Go SDK Stateless, scale replicas without limit behind Nginx, Traefik or ALB
geomilli-harvester Fetch PBF, Osmium filter, transform, Wikidata enrich, index with atomic swap Go 1.25, osmium-tool, SQLite WAL cache Single instance only, scale vertically plus HARVESTER_CONCURRENCY
Meilisearch engine Live places index plus shadow build index LMDB disk store, local or Meilisearch Cloud Needs fast NVMe for planet scale
geomilli-studio Map playground, importer control, telemetry charts React 19, TypeScript, Tailwind, MapLibre GL Static frontend pointing at API port 8080
docs Architecture, pipeline, API reference, studio guide VitePress with Mermaid plugin Served from GitHub Pages

Data flow is linear: client apps hit the Go API, the API queries the live Meilisearch index, the harvester builds a shadow index from OSM streams, and a single swap call promotes the shadow to live with zero downtime.

Core Feature 1: Search and Geospatial REST API

Sub-10ms prefix autocomplete

GET /v1/autocomplete?q=Brux&lang=fr returns keystroke suggestions with typo tolerance, localized display names and geo-proximity ranking. A typical hit carries id, display name, class and subclass, country code, per-language names, lat/lng object and a popularity score derived from Wikipedia pageviews. Reported internal timing is around 2 ms.

curl -s "http://localhost:8080/v1/autocomplete?q=Brux&lang=fr" | jq .

For ham tools this powers callsign-adjacent search boxes: type part of a summit, park or town name and get ranked completions before the operator finishes typing.

Full-text and faceted search

GET /v1/search queries across destinations, categories and attributes with live facet counts. Example from the docs filters restaurants by class, cuisine, star rating and country:

curl -s "http://localhost:8080/v1/search?class=Restaurant&cuisine=italian&star_rating=4&country=BE" | jq .

The same facet machinery filters places by the 19 canonical taxonomy classes (City, TrainStation, Hotel, Restaurant, Airport, Beach, Stadium and others), which lets a ham app query only summits, parks, stations or airports.

Reverse geocoding

GET /v1/reverse?lat=50.8467&lng=4.3525 resolves coordinates into enclosing admin hierarchy plus nearest landmarks. This is the endpoint APRS dashboards, logger maps and telemetry decoders need: turn a received lat/lng into “near X town, Y district” for display.

curl -s "http://localhost:8080/v1/reverse?lat=50.8467&lng=4.3525" | jq .

Nearby POIs by radius

GET /v1/nearby?lat=...&lng=...&radius_meters=1500&class=TouristAttraction returns places inside a radius in meters, sortable by distance or popularity. Replace the class with stations, shelters, hospitals or campsites and the same call answers “what is around my activation site” for portable ops.

curl -s "http://localhost:8080/v1/nearby?lat=50.8467&lng=4.3525&radius_meters=1500&class=TouristAttraction" | jq .

Aviation and airport resolution

GET /v1/airports/BRU looks up by path code, with query filters for IATA, ICAO and country plus limit control. DXpedition and contest travel planning can resolve 3-letter IATA and 4-letter ICAO codes locally instead of scraping airline sites.

curl -s "http://localhost:8080/v1/airports/BRU" | jq .
curl -s "http://localhost:8080/v1/airports?iata=CRL" | jq .
curl -s "http://localhost:8080/v1/airports?country=BE&limit=25" | jq .

Bounding-box queries (/v1/bbox) complete the set for map viewport fetching: send the visible corners, get back only the places inside.

Core Feature 2: Streaming Ingestion Engine

The harvester is a streaming pipeline plus an HTTP/SSE control daemon:

  1. Fetch: streams .osm.pbf country extracts from Geofabrik mirrors for requested countries.
  2. Osmium filter: runs native C++ osmium tags-filter to keep admin boundaries, transport hubs, amenities and tourism features per config/taxonomy.yaml.
  3. Transform: converts geometry to .geojsonseq via osmium export, then emits line-delimited NDJSON batches mapped to 19 canonical classes and filtered through config/languages.yaml.
  4. Enrich: resolves Wikidata IDs, pulls Wikipedia pageviews for popularity scoring, extracts airport IATA/ICAO codes, caches everything in local SQLite WAL cache, and trips a circuit breaker that disables online enrichment after 5 straight network errors.
  5. Index and atomic swap: batches NDJSON into an isolated shadow index named places__build_<timestamp>, configures searchable and filterable attributes, validates, then promotes with a zero-downtime POST /swap-indexes.

Intermediate PBF and NDJSON staging lives on local disk (raw/, staging/), which is why the harvester wants fast local NVMe and can’t share one SQLite file across replicas without SQLITE_BUSY lock contention.

Geomilli Studio: Map Playground plus Ops Console

Studio has three jobs:

  • API playground: test autocomplete, search, reverse, nearby, bbox and airports against a live MapLibre vector map, with a one-click cURL generator targeting the API on port 8080.
  • Importer control center: launch and watch country ingestion jobs with real-time Server-Sent Events progress, country dedup locks and daemon logs.
  • Telemetry dashboard: track QPS, latency percentiles (p50, p95, p99), zero-hit queries and Meilisearch index health with no external APM.

Default ports are API 8080, harvester 8085, Meilisearch 7700, Studio 3333 (or 3000). Useful Makefile targets are make up, make down, make logs, make test, make build, make docs, make docs-build and make up-cloud.

60-Second Quickstart

Default config ingests Belgium (about 70 MB, ready in 60 to 90 seconds):

git clone https://github.com/geomilli/geomilli.git
cd geomilli
cp .env.example .env
docker compose up -d --build

Then open the API at http://localhost:8080, harvester at http://localhost:8085, Meilisearch at http://localhost:7700, Studio at http://localhost:3333, and docs at https://obaid22192.github.io/geomilli/. Set COUNTRIES in .env for more extracts, raise HARVESTER_CONCURRENCY (default 2) for parallel country jobs on a strong host, or point MEILI_HOST and MEILI_API_KEY at Meilisearch Cloud and start with the cloud compose override.

How Amateur Radio Operators Can Use Geomilli

1. SOTA, POTA and WWFF activation planning

Portable awards live or die on place data: summit names, park boundaries, parking, trailheads, nearby towns for access directions. Ingest the target country extract, then use autocomplete for summit and park lookup, nearby radius search for parking and campsites around the reference coordinates, and reverse geocoding to confirm the parking pin sits inside the right admin area. A club activation page can offer “type three letters, pick the peak” with no Google Places bill.

2. Self-hosted gazetteer for loggers and cluster maps

Logging tools constantly need “QTH name to coordinates” and back. Geomilli gives both directions locally: autocomplete and search for forward lookup, reverse for coordinate to place name. Multilingual names (names_by_language) handle border areas and DX QTHs where the English, French and Dutch names differ. Popularity scoring keeps big cities above same-named hamlets in results.

3. APRS and telemetry map labels

An APRS-IS dashboard receiving position packets can call /v1/reverse per fix and render “9M2PJU-9 near Rawang, Selangor” instead of bare digits. Nearby search adds context layers: nearest town, station or shelter within X meters. Because the API is stateless and horizontally scalable, a busy net night just means more API replicas behind Nginx.

4. Repeater, club and net finder backend

Ingest a country, filter to relevant classes, and build a “find my nearest” page: nearby search around the browser geolocation sorted by distance, faceted search by class for clubs and landmarks used as net venues. Pair with your own repeater table for RF data and let Geomilli supply the place layer (town names, admin areas, coordinates) so the directory stays consistent.

5. DXpedition and contest travel routing

The airports endpoint resolves IATA and ICAO codes offline from your own stack. Plan the “fly to X, drive to the island” leg with country=BE&limit=25 style enumeration, confirm the destination airport coordinates, then radius-search accommodation and transport hubs around it. No airline site scraping, no rate limits.

6. EmComm and offline field dashboards

Emergency communications teams need hospitals, shelters, stations, stadiums and admin boundaries when the internet is degraded. A pre-ingested Geomilli instance on a laptop or Raspberry Pi class host serves autocomplete, reverse and nearby with no external dependency after ingest. The harvester circuit breaker already tolerates flaky links by disabling enrichment after repeated failures while cached data keeps serving. Keep the index built before deployment day, then run the API plus Meilisearch locally at the command post.

7. Why self-host instead of Nominatim, Photon or Google

Option Hosting Speed profile Data control Cost and limits
Geomilli Self-hosted Docker stack, Apache-2.0 Sub-10ms autocomplete via Meilisearch Choose countries, taxonomy, languages, popularity model No per-query bill, hardware cost only
Nominatim Self-host or public API Correct but heavier, Postgres based Full OSM planet logic, complex setup Public API has strict usage policy
Photon Self-host Java plus Elasticsearch Fast typeahead over OSM Good defaults, less tunable ranking JVM plus ES footprint
Pelias Self-host microservices plus ES Strong geocoder, many moving parts Very configurable, complex ops Heavy multi-container ops burden
Google Places/Geocoding Cloud only Excellent None, terms can restrict storing results Per-request billing, key management

Geomilli fits hams who want a small number of countries, typo-tolerant typeahead, airport codes and a MapLibre playground in one compose file, without running a full planet Nominatim database.

Practical Tips and Limits

  • Start with one small country (defaults to BE) and confirm the pipeline before ingesting large neighbors. Malaysia (MY) and neighbors follow the same COUNTRIES pattern via Geofabrik paths.
  • Give the harvester fast local disk and enough cores; it is single-instance by design, so scale up the host and raise HARVESTER_CONCURRENCY rather than adding harvester replicas.
  • Scale the API freely: it holds no local state, so add replicas behind any reverse proxy.
  • Watch telemetry for zero-hit queries: they reveal missing taxonomy entries or language gaps worth adding to taxonomy.yaml and languages.yaml.
  • Respect licenses: code is Apache-2.0, OpenStreetMap data stays under ODbL with attribution.
  • The repo is young (7 commits, 2 stars at write time), so pin a release commit, test upgrades on a spare host, and keep Geofabrik plus Wikidata outage fallbacks in mind for enrichment.

Frequently Asked Questions

What does Geomilli do in one sentence?

Geomilli converts OpenStreetMap country extracts into a fast self-hosted search API with autocomplete, reverse geocoding, nearby search and airport lookup backed by Meilisearch.

How fast is Geomilli search?

Autocomplete is designed for sub-10ms responses with typo tolerance and popularity ranking. The API itself is stateless Go, so extra replicas behind a proxy handle contest or net-night load.

What hardware runs a country ingest?

Any Docker host with decent CPU and fast local disk works. Belgium at about 70 MB finishes in roughly a minute. Bigger countries want NVMe staging and higher harvester concurrency on a single instance.

Can Geomilli work offline at a field station?

Yes, after ingest. Build the index with connectivity, then run the API plus Meilisearch locally at the site for place search, reverse labels and nearby POIs with no external calls.

How can hams use Geomilli concretely?

Use it for SOTA and POTA site lookup, APRS coordinate-to-place labels, repeater and club finder place layers, DXpedition airport routing, and offline EmComm dashboards for shelters and stations.

Sources and Further Reading

73 de 9M2PJU. Host your own map intelligence, then take it portable.

Post Comment