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.

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:
- Fetch: streams
.osm.pbfcountry extracts from Geofabrik mirrors for requested countries. - Osmium filter: runs native C++
osmium tags-filterto keep admin boundaries, transport hubs, amenities and tourism features perconfig/taxonomy.yaml. - Transform: converts geometry to
.geojsonseqviaosmium export, then emits line-delimited NDJSON batches mapped to 19 canonical classes and filtered throughconfig/languages.yaml. - 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.
- 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-downtimePOST /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 sameCOUNTRIESpattern 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_CONCURRENCYrather 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.yamlandlanguages.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
- Geomilli repository by obaid22192: https://github.com/obaid22192/geomilli
- Geomilli documentation site: https://obaid22192.github.io/geomilli/
- Meilisearch search engine: https://www.meilisearch.com
- OpenStreetMap copyright and license: https://www.openstreetmap.org/copyright
- Geofabrik OSM extracts: https://download.geofabrik.de/
- Osmium tool manual: https://osmcode.org/osmium-tool/
- MapLibre GL JS: https://maplibre.org/
- Nominatim usage policy: https://operations.osmfoundation.org/policies/nominatim/
- Summits on the Air programme: https://www.sota.org.uk/
- Parks on the Air programme: https://parksontheair.com/
73 de 9M2PJU. Host your own map intelligence, then take it portable.



Post Comment