VibeEats is a small food-discovery app: pick a mood ("cozy," "date night,"
"quick bite"), or search for what you're craving, and it matches nearby
places that are actually open, weighted by the weather and time of day.
I want to write about the part that was actually interesting to build: it
runs on $0 in API costs. Here's how, and what broke along the way.
The problem
The first version used Google Places for search, Google Geocoding for
locations, and Google Weather. A single user search fanned out into a
Nearby Search call plus ~20 Place Details calls (for photos, hours, ratings)
plus photo fetches. That's the standard Google Places pattern, and it adds
up fast — the bill got bad enough that I turned the whole discovery feature
off for a few months and left up a bare coupon page.
The rebuild: free, public data only
- Places: OpenStreetMap via the Overpass API — free, keyless, and the data is often better-tagged than I expected (more on that below)
- Geocoding: Photon with Nominatim as a fallback
- Weather: Open-Meteo (free tier is licensed for non-commercial use, worth knowing if you're building something you'll monetize)
No API keys anywhere in that stack. The only paid things left in the whole
app are hosting (Vercel) and the database (Supabase), both well within free
tiers at this scale.
The catch: no ratings, no photos, no prices
Google/Yelp give you a star rating, photos, and a price level out of the
box. OpenStreetMap gives you none of that. So instead of faking it, I built
the ranking entirely from what's actually tagge
features (outdoor seating, takeaway, wheelchair access...), distance, and
current weather/time of day. Roughly:
\ts
// simplified — the real scoring blends ~6 weighted signals
function scorePlace(place, vibe, weather, hour)
const kindFit = KIND_WEIGHTS[vibe][place.kind] ?? 0.1;
const cuisineFit = bestCuisineMatch(place.cuiutral, not punished, if untagged
const weatherFit = place.hasOutdoorSeating && isNiceOut(weather) ? 0.35 : 0;
const openBonus = isOpenNow(place) ? 8 : -25;
return blend([kindFit, cuisineFit, weatherFit]) + openBonus + proximityScore(place.distance);
}
\\
I did the same thing for price. There's a $/$$/$$$ filter in the app now,
and before building it I actually checked: I pus
across three cities (Austin, Bengaluru, New York) and looked for any
price-related OSM tag. Zero had one. Not pathe
price filter is a heuristic from venue type + cuisine + chain status,
labeled "estimated" everywhere it shows up, nev
number. It's a small thing, but I think "check before you build" mattered
more here than usual — it would've been easy to
levels exist somewhere in OSM and ship something that quietly lied.
Caching, because Overpass is slow
Public Overpass instances are free but not fast — a cold query can take
10–30 seconds, and they rate-limit. Calling tha
non-starter. So places are cached by geo-cell: the world's cut into ~2km
tiles (falling back to ~9km in sparse areas), e
served to everyone after that. Cache layers are memory → local disk →
Supabase, in that order, so a serverless cold sond
wait for every visitor.
Search over tagged data has a fun problem
Free-text search ("biryani near me") searches within the places already
loaded for the area — no extra API call. The inap
tags dishes more literally than I expected. Querying restaurants near
Hyderabad, India, for cuisine=biryani returns
not everywhere — so there's a small synonym table (biryani → also match
indian/pakistani/mughlai cuisine tags) as
specific dish isn't a literal tag. Tested against real data, it correctly
surfaced well-known local biryani spots that weot
biryani — the fallback earning its keep, not just theoretical.
What I'd tell someone trying this
- OSM data quality varies a lot by city — dense in places with an active local mapping community, thin elsewhere. Buil (fall back to a wider search radius) rather than assuming uniform density.
- Don't call public Overpass instances per-reqund consider hedging across mirrors since any single instance can be flaky.
- If a data source doesn't have a field you wan, resist the urge to synthesize something that looks authoritative. Label estimates as estimates.
Happy to answer questions about any of this, es
built on top of Overpass/OSM data before — curious what I'm missing.
Top comments (0)