Location-based discovery doesn't require a metered maps platform for every query. Here is how we built geo-search for a multi-city rental marketplace on a fixed hosting cost.
By Team WebSync · · 4 min read

"Show me listings near me" sounds like a one-line feature. In practice it's a query pattern most databases aren't built for, and a pricing trap if you reach for a metered geolocation API on every search and every page load.
We ran into this building a co-living rental marketplace live across three cities. Renters filter by neighbourhood constantly - on the initial search, and again every time they adjust a radius or move the map. That's not a handful of geocoding calls a day, it's thousands, and a per-request maps bill scales directly with your traffic instead of your revenue.
The expensive part of geo-search is turning an address into coordinates - not searching by coordinates. So we only geocode once, when a listing is created or edited, and store latitude/longitude directly on the property record. Every search afterwards is a pure database query against numbers already on disk, with zero API calls.
The one exception is the user's own search location, which we resolve client-side from the browser's Geolocation API (free) or a typed address geocoded on submit - a few requests per session, not one per listing rendered.
You don't need PostGIS or Elasticsearch to get correct, fast "nearest to me" results at marketplace scale. A bounding-box pre-filter (latitude/longitude between a min and max, sized to roughly the search radius) narrows a full table down to a few hundred candidate rows using ordinary indexed range queries. The Haversine distance formula then runs only against that narrowed set, in application code, to get an accurate distance and sort order.
function boundingBox(lat, lng, radiusKm) {
const latDelta = radiusKm / 111; // ~111km per degree latitude
const lngDelta = radiusKm / (111 * Math.cos(lat * Math.PI / 180));
return {
minLat: lat - latDelta, maxLat: lat + latDelta,
minLng: lng - lngDelta, maxLng: lng + lngDelta,
};
}
function haversineKm(lat1, lng1, lat2, lng2) {
const R = 6371;
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLng = (lng2 - lng1) * Math.PI / 180;
const a = Math.sin(dLat / 2) ** 2 +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLng / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}This two-stage approach - a cheap index-backed rectangle filter, then precise distance math on the survivors - is what makes nearest-me search fast without a dedicated spatial engine. It scales comfortably into the tens of thousands of listings per city before a real spatial index becomes worth the operational overhead.
Rendering the actual map tiles is the one piece that legitimately needs a maps vendor. We keep that bill flat by loading the map only when a user explicitly switches to map view - the default list view needs no maps SDK at all - and by capping tile requests with viewport-based lazy loading instead of re-rendering on every scroll pixel.
The maps bill scales with API calls, not with users. Design so distance is math you already have the numbers for, not a network request you make on every keystroke.
Geocode addresses once at write time and store coordinates directly on each record, so search never calls an external API. Pre-filter with an indexed bounding-box query, then compute exact Haversine distance only on that narrowed set in application code. Reserve the paid maps SDK for rendering tiles on an explicit map view, not for every search request.
Book a free consult - we'll scope it and give you a fixed price.