geo
Multi-provider geocoding abstraction with automatic fallback chain, DB-backed caching, per-provider rate limiting, and Haversine distance calculation.
Key namespaces
| Namespace | Layer | Responsibility |
|---|---|---|
|
shared |
Malli schemas: |
|
shared |
|
|
core |
|
|
core |
|
|
shell |
Nominatim adapter (no API key required) |
|
shell |
Google Maps adapter (requires |
|
shell |
Mapbox adapter (requires |
|
shell |
PDOK Locatieserver adapter: Dutch addresses, no API key |
|
shell |
|
|
shell |
Public API: |
Configuration
;; OpenStreetMap only (no API key)
:wagoe/geo-service
{:provider :openstreetmap
:user-agent "MyApp/1.0 (contact@example.com)"
:cache-ttl 86400 ; 24 hours
:cache? true}
;; PDOK: street, city and point from postcode + house number (no API key)
:wagoe/geo-service
{:provider :pdok
:cache-ttl 86400
:cache? true}
;; Fallback chain: OSM first, then Google
:wagoe/geo-service
{:provider [:openstreetmap :google]
:api-key #env WAG_GEO_API_KEY
:user-agent "MyApp/1.0"
:cache-ttl 86400
:cache? true}
:cache? true caches lookups in geo_cache through :wagoe/db-context, which the
module wires itself; run the migration first. A config that does not match
wagoe.geo.schema/GeoConfig fails at boot.
Usage
(require '[wagoe.geo.shell.service :as geo])
;; Geocode an address
(geo/geocode! service {:address "Prinsengracht 263, Amsterdam"})
;=> {:lat 52.3747 :lng 4.8844 :formatted-address "Prinsengracht 263, Amsterdam" :provider :openstreetmap :cached? false}
;; Postcode + house number (PDOK)
(geo/geocode! service {:postcode "1012 JS" :house-number 1 :house-number-addition "A"})
;=> {:street "Dam" :house-number "1" :house-number-addition "A" :city "Amsterdam" :lat ... :lng ...}
;; Only the named providers, for this call; cached separately
(geo/geocode! service {:postcode "1012 JS" :house-number 1} {:only #{:pdok}})
;; The same through the component's port, without requiring the shell namespace
(require '[wagoe.geo.ports :as geo-ports])
(geo-ports/lookup service {:postcode "1012 JS" :house-number 1} {:only #{:pdok}})
;; Reverse geocode
(geo/reverse-geocode! service {:lat 52.3747 :lng 4.8844})
;=> {:formatted-address "Prinsengracht 263, 1016 GV Amsterdam"}
;; Haversine distance (pure, no HTTP)
(geo/distance {:lat 52.3747 :lng 4.8844} {:lat 52.3702 :lng 4.8952})
;=> 0.84 ; kilometres
Rate limiting and timeouts
-
OpenStreetMap (Nominatim): 1 request/second enforced
-
Google / Mapbox / PDOK: 100 ms between requests
-
One adapter per provider, shared across threads: the limit is per instance. A waiter holds no slot, so a cancelled lookup delays no one.
-
:timeout-ms(default 3000) bounds each call’s connect and read; a timeout is a miss, logged as an ERROR, and the chain moves on.
DB migration
Apply the geo_cache table migration before first use:
clojure -M:migrate up
Migration files: resources/wagoe/geo/migrations/ (20260324010000-geo-cache.up.sql and later)
Testing
clojure -M:test :geo