Engineering
Self-Hosting Pelias: Geocoding for African Addresses
How we deployed Pelias geocoder on a single VM to handle descriptive African addresses that Google Maps can't resolve — for $5/month.

Google Maps can’t find “behind the Shell garage, Dangamvura, Mutare.” But the rider who needs to deliver there knows exactly where it is. The problem isn’t the address — it’s the geocoder.
Most geocoders assume structured addresses: street number, street name, city, postal code. African addresses are descriptive: landmarks, neighborhoods, relational directions. You don’t live at “123 Main Street” — you live “near the butcher shop in Chitungwiza.”
We solved this by self-hosting Pelias. Here’s how.
Why Pelias
Pelias is an open-source geocoder built by Mapzen. It’s designed to be self-hosted and uses OpenStreetMap (OSM) data as its primary source. Two things made it the right choice:
- OSM data in Africa is often better than Google’s. Local mappers know the landmarks, the neighborhoods, the informal paths. Google’s data is collected from satellites and official sources — it misses the ground truth.
- Fuzzy matching by default. Pelias doesn’t require exact matches. It does fuzzy text matching against a trie-based index, which handles “Dangamvura” vs “Dangambura” vs “Dangmvura” naturally.
The Deployment: One VM, $5/month
We run Pelias on a single Hetzner VM (CX21, 4GB RAM, 80GB disk). It handles our entire geocoding load — ~10,000 requests/day — with room to spare.
The data pipeline:
#!/bin/bash
# Download OSM data for Zimbabwe
wget -O data/zimbabwe.osm.pbf \
"https://download.geofabrik.de/africa/zimbabwe-latest.osm.pbf"
# Import into Pelias
docker compose run --rm whosonfirst import
docker compose run --rm openstreetmap import
docker compose run --rm openaddresses import
docker compose run --rm polity一千 import
# Build the index
docker compose run --rm elasticsearch create-indices
docker compose run --rm interpolation build
The full import takes ~2 hours and uses ~40GB of disk. The resulting index fits in 2GB of RAM with Elasticsearch’s mmap-based caching.
The API: Simple, Fast, Good
# 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['properties']['label'],
coordinates: feature['geometry']['coordinates'],
confidence: feature['properties']['confidence'],
source: feature['properties']['source']
}
end
end
end
The In-Memory Cache
For Muchiround, we see the same addresses repeatedly — a provider’s regular stops don’t change often. We added a simple in-memory LRU cache:
# app/services/geocode_cache.rb
class GeocodeCache
CACHE_KEY = 'geocode_cache'
MAX_SIZE = 10_000
def initialize
@cache = ActiveSupport::Cache::MemoryStore.new(
size: MAX_SIZE,
expires_in: 24.hours
)
end
def fetch(address)
key = "#{CACHE_KEY}:#{Digest::MD5.hexdigest(address.downcase)}"
@cache.fetch(key) do
yield
end
end
def clear
@cache.clear
end
end
This cuts our Pelias requests by ~60%. The cache hits resolve in microseconds; misses hit Pelias in ~50ms.
Handling the Worst Case
Some addresses just can’t be geocoded. “Turn left at the blue house after the tuckshop” has no coordinates. For these, we fall back to manual placement:
- The rider app has a “drop pin” mode. The rider drops a pin at the actual location.
- The pin is saved with the original descriptive address as a reference.
- Next time that address is entered, the pin is used.
Over time, this builds a custom geocoding layer on top of Pelias — one that gets more accurate with every delivery.
The Cost Comparison
| Solution | Monthly Cost | Coverage | Latency |
|---|---|---|---|
| Google Maps API | $200+ (10k requests) | 40% hit rate in Zimbabwe | 200ms |
| Pelias (self-hosted) | $5 (VM) | 75% hit rate in Zimbabwe | 50ms |
| Pelias + manual pins | $5 (VM) | 95% hit rate | 20ms (cached) |
The 75% hit rate is the out-of-the-box number. With manual pins accumulated over 3 months, we’re at 95%. Google Maps was at 40% because it couldn’t handle descriptive addresses at all.
The Bigger Point
Africa doesn’t need Western infrastructure. It needs infrastructure that understands the terrain. Self-hosted Pelias is our geocoding layer — not because it’s cheaper (it is), but because it’s better for our context.
The assumption that proprietary services are always better is wrong. When your problem doesn’t match their assumptions, open source wins. African addresses don’t match Google’s assumptions. So we built our own.
For $5 a month.
Kudapara Kady
Founder & Engineer
Building software for Africa at Kudapara. Engineering, AI, and logistics from the ground.
Keep Reading

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.

Jul 28, 2026
Production-ready. Still fast.
How Kudapara ships with Hermes agents, Kanban handoffs, PR discipline, local CI merge gates, and the Algorithm before automation.