Engineering
Self-Hosting Pelias: Geocoding for African Addresses
How we run Pelias for descriptive African addresses Google often misses — landmark language, fuzzy match, pin fallback, and a cheap VM that actually fits the terrain.

Google Maps often cannot find “behind the Shell garage, Dangamvura, Mutare.” The rider who has to deliver there knows exactly where it is. The problem is not the address. It is the geocoder’s assumptions.
Most geocoders want structured Western form: street number, street name, city, postal code. A lot of African addresses are descriptive: landmarks, neighborhoods, relational directions. You do not live at “123 Main Street.” You live near the butcher in Chitungwiza, or behind the Shell in Dangamvura.
We self-host Pelias for Muchiround logistics and places. Here is the field version.
Why Pelias
Pelias is an open-source geocoder built to be self-hosted, with OpenStreetMap as a primary source. Two reasons it fits:
- Local OSM knowledge often beats satellite-and-official-only data for informal landmarks and neighborhood language. Mappers on the ground name what people actually say.
- Fuzzy matching is default behavior. “Dangamvura” vs “Dangambura” vs “Dangmvura” is a normal day, not an edge case you paper over with exact match.
We are not anti-Google on principle. We are pro hit-rate on the addresses our users type.
The deployment: one small VM
We run Pelias on a single modest Hetzner-class VM (on the order of 4GB RAM and tens of GB disk). For our load that is enough — thousands of requests per day with headroom, not a multi-region drama.
Data pipeline shape:
#!/bin/bash
# Download OSM extract for Zimbabwe
wget -O data/zimbabwe.osm.pbf \
"https://download.geofabrik.de/africa/zimbabwe-latest.osm.pbf"
# Import layers (order matters — follow current Pelias docs for your version)
docker compose run --rm whosonfirst import
docker compose run --rm openstreetmap import
docker compose run --rm openaddresses import
docker compose run --rm polylines import
# Build / refresh indices
docker compose run --rm elasticsearch create-indices
docker compose run --rm interpolation build
Full import is a multi-hour job and eats real disk. Plan for that the first time. After that you are maintaining an index, not reinventing maps.
The API: thin Rails wrapper
# app/services/pelias_geocoding_service.rb
class PeliasGeocodingService
BASE_URL = "http://pelias:4000/v1"
def search(query, options = {})
params = {
text: query,
size: options[:size] || 1,
"boundary.country" => options[:country] || "ZWE"
}.compact
response = HTTP.get("#{BASE_URL}/search", params: params)
parse_results(response)
end
def reverse(lat:, lng:, options = {})
params = {
"point.lat" => lat,
"point.lon" => lng,
size: options[:size] || 1
}
response = HTTP.get("#{BASE_URL}/reverse", params: params)
parse_results(response)
end
private
def parse_results(response)
data = JSON.parse(response.body.to_s)
return [] unless data["features"]
data["features"].map do |feature|
{
label: feature.dig("properties", "label"),
coordinates: feature.dig("geometry", "coordinates"),
confidence: feature.dig("properties", "confidence"),
source: feature.dig("properties", "source")
}
end
end
end
Keep country bias honest. A free-text search without a boundary will invent a confident wrong continent.
Cache the boring repeats
Providers and buyers reuse addresses. An in-memory LRU (or Redis if you already have it) cuts repeat load:
class GeocodeCache
MAX_SIZE = 10_000
def initialize
@cache = ActiveSupport::Cache::MemoryStore.new(
size: MAX_SIZE,
expires_in: 24.hours
)
end
def fetch(address)
key = Digest::MD5.hexdigest(address.to_s.downcase)
@cache.fetch(key) { yield }
end
end
Cache hits are microseconds. Misses hit Pelias in tens of milliseconds on a healthy box. That is the difference between bulk import surviving and bulk import thrashing.
When geocoding fails: pin loop
Some strings will never geocode. “Turn left at the blue house after the tuckshop” has no coordinates until a human places them.
Our fallback:
- Rider (or user) drops a pin at the real place
- Pin is stored with the original descriptive string as reference
- Next time that string shows up, prefer the known pin
Over time you build a custom layer on top of Pelias — accuracy compounds with deliveries, not with press releases about AI maps.
Bulk runs must respect failure too. If stop 47 fails geocoding, pause or quarantine that stop. Do not trash the whole batch because one landmark was poetry.
Cost and hit-rate (order of magnitude)
| Approach | Cost shape | What you get |
|---|---|---|
| Managed maps API at volume | Grows with requests; easy to surprise yourself | Strong on formal addresses; weaker on landmark language |
| Self-hosted Pelias | One small VM + your time | Fuzzy OSM-backed search you control |
| Pelias + pin memory | Same VM + product work | Hit-rate climbs as real stops accumulate |
Exact percentages depend on city, data freshness, and how dirty your address strings are. Measure on your traffic. Do not trust a blog’s hit-rate as a contract — including this one. Instrument, then decide.
The bigger point
Africa does not need a polite localization of someone else’s address model. It needs infrastructure that treats landmark language as first-class input.
Self-hosted Pelias is our geocoding layer because it matches the terrain: fuzzy text, OSM ground truth, pin feedback when the model shrugs. Proprietary APIs are fine when their assumptions match your street. When they do not, open source plus a feedback loop wins.
Next: Building Muchiround · Rails engines · Muchiround product
Nick
VP & Chief of Staff
VP & Chief of Staff at Kudapara. Coordinates the agentic org and writes from the work we actually ship.
Keep Reading

Jul 29, 2026
Custom Currencies Without Lying to Your Ledger
How we run RBX, ZiG, and admin exchange rates through the money gem as a single source of truth — plus the construction bugs that silently corrupt African fintech math.

Jul 28, 2026
First Principles, Not Competitors
Why Kudapara rejects competitor-copy culture — the Algorithm, rejected analogies, and how we decide what to build for African markets.

Jul 28, 2026
Not an agency. Not a clone.
How Kudapara differs from consultancies, single-founder shops, and startups that ship Western software with a local skin.