The pitch for the feature was simple: let users drag a map, draw a rough search area, and see listings inside it. The complication showed up the moment we priced it out. At the traffic volumes a growing proptech product actually sees, Google Maps' per-load pricing stopped being a rounding error and started being a real line item — and in several of the neighborhoods we cared about most, the map data itself was thin: missing roads, unnamed streets, buildings that didn't exist on the map at all. Paying more for coverage that wasn't there wasn't a trade we were willing to make twice.
The stack we landed on
We rebuilt the mapping layer on open data instead of a metered API:
- Tiles and rendering: MapLibre GL for the client, serving vector tiles generated from OpenStreetMap extracts instead of a hosted, per-load provider. Self-hosting tiles turned an unpredictable per-request cost into a flat infrastructure cost.
- Geocoding: Nominatim, self-hosted against a regional OSM extract, for turning addresses into coordinates. Coverage still had gaps, but they were gaps we could see and patch, not a black box.
- Spatial search: PostGIS on top of Postgres, indexed with GiST, for "find listings inside this polygon" queries — the actual query behind drag-to-search.
- Area indexing: H3 hexagonal indexing for aggregating listings into search regions, so "how many listings in this area" didn't mean scanning every row on every map pan.
-- listings inside a user-drawn search polygon
SELECT id, title, price
FROM listings
WHERE ST_Within(
location,
ST_GeomFromGeoJSON(:search_polygon)
);
The problem that actually cost us time: addresses that don't resolve
The map rendering and the spatial query were the easy half. The hard half was that a meaningful share of listings didn't have an address that any geocoder — ours or Google's — could resolve cleanly. Many properties are described relative to a landmark ("second gate past the blue church") rather than a street address, because that's how the neighborhood actually navigates, address plates or not.
Forcing agents to type a formal address produced garbage coordinates, because they'd guess. What worked instead was flipping the input around: let the agent drop a pin directly on the map at the property's real location, and treat that pin as the source of truth. Geocoding ran in the background only to generate a human-readable label for display — never to determine where the listing actually showed up in a search. The map became the primary key for location; the address became decoration.
Handling sparse road data gracefully
Where OSM road coverage was thin, straight-line "as the crow flies" distance was a more honest default than routing through a road network that might not be mapped yet. We showed straight-line distance with a visual indicator that it was approximate, rather than presenting a driving-distance number computed against incomplete road data that would look precise and be wrong. An honest approximation beat a confident, broken calculation.
What this bought us
Dropping a metered map API removed a cost that scaled with usage in a business where usage growth is the entire point. More importantly, treating OSM data as something we could inspect and patch — instead of a paid black box — meant that coverage gaps became a backlog item instead of a permanent limitation. We still contribute fixes back to OSM in areas we operate, because every correction we make is a correction every other OSM-based product in the region also gets for free.
The lesson that generalized past this one project: in markets that mapping providers treat as an afterthought, the "convenient" managed API is often the one with the worst underlying data. Open, self-hosted, and inspectable can be the more reliable choice, not just the cheaper one.
I write about building products for markets the default tooling wasn't designed for, in my newsletter, AI Shipped. New issue every week.