Locations¶
A location in Tomoda is a real place in the world — a bar, a park, a corner, a venue — and it is the primitive everything spatial in the product hangs off. Events happen at locations. Moments are pinned to locations. The map is a layer of locations. Locations are not a secondary attribute; they're a first-class object with their own lifecycle.
The story¶
The naive way to build a location-aware product is to ask Google Places for every coordinate and call it done. We didn't, for three reasons. First, the bill scales linearly with usage and we don't want product decisions hostage to a rate-limited external API. Second, every user query against the same place wastes a request. Third, we want our own canonical record of a place — with multi-language names, our own moment-count and rating signal — that we own, that we can localise, and that doesn't break if Google changes their data shape.
So the location stack is a three-tier pipeline. The cheap tier
is Photon, the self-hosted OpenStreetMap
geocoder — fast, free, no rate limit, decent for nearby search and
text autocomplete. We hit it freely. The next tier is our own
Postgres locations table — a place we've seen before, with all
its enrichment. The expensive tier is Google Places, called only
when a user resolves a candidate into a persisted location and the
existing data is stale or missing. That call gets the ratings,
hours, photos, business status. Once stored, the location is fresh
for 30 days and serves every subsequent request out of Postgres.
Two design choices fall out of this. One: every location has a multi-language name. When a location is first resolved we fan Photon out across 28 languages in parallel and store the lot in a JSONB column. The wall-clock cost is one Photon RTT (parallel) and every subsequent read is free. We trim the response to the caller's language fallback chain so payloads stay small. Two: every location dedupes by proximity. A "new" moment dropped at the corner of a park within a few metres of an existing location matches the existing record rather than spawning a duplicate; the PostGIS geography index makes the nearby check cheap.
The third shape is verification status. A location is
google_verified (the gold standard), photon_matched (OSM-sourced,
no Google enrichment yet), or user_reported (a user pinned a
coordinate that wasn't in either source). The status surfaces in the
UI as a slightly different affordance — a Google-verified venue gets
hours and photos; a user-reported pin gets the bare coordinate plus
whatever the user typed.
What we're optimising for¶
- Own the canonical record. Locations are our table, not a third-party API call.
- Cheap tier first. Photon is free and unlimited; Google is the enrichment step, called only when needed.
- 30-day freshness window. A location served fresh from Postgres; refreshed from Google when it crosses the staleness line.
- Multi-language by default. 28 languages, indexed once on resolve, trimmed at read time.
- Proximity for spatial dedup. Per-category radii on the PostGIS index make "same place" cheap to compute.
- Photon outages are silent. The geocoder dropping a request must never break a user flow — fall through to Google or to a minimal record.
What a location is not¶
- It is not a user-owned entity. A location is the same row whether it's referenced by one event or a hundred moments. Users don't "own" locations.
- It is not a check-in destination on its own. A user doesn't "check in" to a location — they post a moment or join an event at one. The location is the substrate, not the action.
- It is not a directory we sell or expose as an API.
- It is not a private place. Locations are inherently public (real-world venues); privacy lives on the things attached to a location (the friends-only moments, the invite-only events).
See also¶
- Domain spec: Domains → Locations
- Backend implementation: Backend → Locations service
- Geocoder ops: Infrastructure → Photon
- How locations feed the map: Bible → Discovery
- UX for picking, resolving, pinning:
../ux/locations.md(coming soon)