|
| 1 | +# ADR-0006: POI provider strategy — hybrid by category, phased rollout |
| 2 | + |
| 3 | +- Status: Accepted (decision); Implementation deferred until user is ready to provision accounts |
| 4 | +- Date: 2026-05-11 |
| 5 | +- Deciders: Tomer |
| 6 | + |
| 7 | +## Context |
| 8 | + |
| 9 | +ADR-0003 chose **Geoapify Places** (OSM-sourced) as the MVP POI provider. After a week of real-world use, an accuracy gap is visible: the user reports specific Tel Aviv places — cafés, restaurants, small businesses — that exist in the city but are missing from the map. This isn't a Geoapify bug; it's an OSM coverage gap. Commercial venue density on OSM has historically lagged commercial map products (Google, Foursquare, Apple) by years, especially outside major Western European cities. |
| 10 | + |
| 11 | +The user also wants to add a "suggested places" panel sortable by user-configurable parameters (popularity, rating, distance). OSM data does not include rating/popularity signals at scale; commercial APIs do. |
| 12 | + |
| 13 | +## Decision |
| 14 | + |
| 15 | +Adopt a **hybrid POI sourcing strategy**, with implementation phased to track when external accounts are available. |
| 16 | + |
| 17 | +### Category routing |
| 18 | + |
| 19 | +Split categories by where the data lives best: |
| 20 | + |
| 21 | +| Category | Best source | Why | |
| 22 | +|---|---|---| |
| 23 | +| Park | OSM (Geoapify) | OSM excels at physical geography; parks are well-mapped. | |
| 24 | +| Beach | OSM (Geoapify) | Same. | |
| 25 | +| Viewpoint | OSM (Geoapify) | Volunteer mappers love viewpoints. | |
| 26 | +| Museum | OSM (Geoapify) or commercial | Static, well-known set; either works. | |
| 27 | +| Café | Foursquare or Google | Commercial venue churn; OSM gaps are biggest here. | |
| 28 | +| Restaurant | Foursquare or Google | Same as café. | |
| 29 | + |
| 30 | +The mapping lives in code as `CATEGORY_TO_PROVIDER`. UI and the rest of the app see one normalized `Poi[]` and don't know which provider produced which marker. |
| 31 | + |
| 32 | +### Architecture |
| 33 | + |
| 34 | +Introduce a `CompositePoiProvider` that wraps multiple sub-providers and routes requests by category: |
| 35 | + |
| 36 | +```ts |
| 37 | +class CompositePoiProvider implements PoiProvider { |
| 38 | + constructor(private byCategory: Partial<Record<PoiCategory, PoiProvider>>) {} |
| 39 | + |
| 40 | + async searchInBbox(q: PoiQuery): Promise<PoiResult> { |
| 41 | + const groups = groupBy(q.categories, (c) => this.byCategory[c]); |
| 42 | + const results = await Promise.all( |
| 43 | + [...groups.entries()].map(([provider, cats]) => |
| 44 | + provider?.searchInBbox({ ...q, categories: cats }), |
| 45 | + ), |
| 46 | + ); |
| 47 | + return mergeResults(results); |
| 48 | + } |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +The route handler instantiates this at startup based on env-var configuration. Single-provider deployments (e.g. "Geoapify only") work without `CompositePoiProvider` by passing the single provider directly. |
| 53 | + |
| 54 | +### Provider preference |
| 55 | + |
| 56 | +Among the commercial options, **Foursquare > Google** as the default upgrade for this project: |
| 57 | +- 100k req/mo free tier vs. Google's $200/mo credit (~10k Nearby Search calls). Foursquare is 10× cheaper at scale. |
| 58 | +- No credit card required for Foursquare's free tier. |
| 59 | +- Permissive ToS — fewer restrictions on caching and display than Google. |
| 60 | +- Comparable venue data quality in dense urban areas, including Tel Aviv. |
| 61 | + |
| 62 | +Google remains a credible second choice and may eventually be added as a third adapter for direct comparison. Apple Maps Server API is excluded (requires paid Apple Developer Program). |
| 63 | + |
| 64 | +### Phasing |
| 65 | + |
| 66 | +- **Phase 0 (now).** Geoapify only. CompositePoiProvider not yet introduced. Accuracy gap documented; user is informed. |
| 67 | +- **Phase 1 (when user provisions Foursquare).** Add `FoursquarePoiProvider`, introduce `CompositePoiProvider`, route commercial categories to Foursquare while keeping geographic categories on Geoapify. Add `FOURSQUARE_API_KEY` to `.env.example`. |
| 68 | +- **Phase 2 (optional).** Add `GooglePoiProvider` for comparison. Could be selected per category, or A/B'd against Foursquare via a feature flag. |
| 69 | +- **Phase 3 (suggested-places panel).** Build the side panel that lists POIs with sortable columns (rating, distance, category). Rating is sourced from Foursquare/Google; distance is computed client-side from `state.origin`; category is already in the `Poi` shape. The panel reuses the existing `PoiProvider` data path — no new provider work needed. |
| 70 | + |
| 71 | +## Why not just add Foursquare now |
| 72 | + |
| 73 | +The user explicitly opted out of provisioning new external accounts this round. Building a Foursquare adapter without an active Foursquare account means landing untested code in the repo, then either: |
| 74 | + |
| 75 | +- Mocking the upstream and relying on integration tests we don't have, or |
| 76 | +- Leaving the code in a "compiles but never ran" state — high risk of subtle bugs when it eventually does run. |
| 77 | + |
| 78 | +Better to write the design now (this ADR + a stub in TASKS.md), and implement when the account is in hand and the integration can be exercised end-to-end. |
| 79 | + |
| 80 | +## Consequences |
| 81 | + |
| 82 | +- Phase 0 ships with a known accuracy gap. The map will under-represent Tel Aviv's commercial venue density. We mitigate by showing the user we know. |
| 83 | +- The PRD's POI categories (FR-6) don't change — internal taxonomy is provider-agnostic on purpose. |
| 84 | +- Phase 1+ adds attribution requirements: any view that displays Foursquare data must show Foursquare attribution near the marker or in the destination card. ADR-0005's `DestinationCard` is the natural home. |
| 85 | +- The CompositePoiProvider pattern is a clean teaching moment for the abstraction: multiple adapters behind one interface, routing by domain key (category) without UI awareness. Worth highlighting in the README screenshots/walkthrough. |
| 86 | +- Cost ceilings: even Phase 2 (Google) stays inside Google's $200/mo credit for portfolio-level traffic. |
| 87 | + |
| 88 | +## When this ADR should be reopened |
| 89 | + |
| 90 | +- If Foursquare's free tier changes meaningfully (currently 100k req/mo). |
| 91 | +- If OSM coverage in Tel Aviv improves to the point that the gap closes (track via spot checks every few months). |
| 92 | +- If the user decides to go all-in on Google Maps for the UI overall (e.g. switching basemap to Google Maps tiles) — at which point a single-provider Google adapter becomes the simpler default. |
0 commit comments