--- title: Build a Neighborhood Cafe Finder | Plaza Docs description: A React app that geocodes an address, finds nearby cafes with outdoor seating, calculates walk times, and plots everything on an interactive map. --- Type an address, get a map showing every cafe with outdoor seating within walking distance, sorted by how long it takes to get there on foot. ## What you’ll use - **Geocoding** to turn an address into coordinates - **PlazaQL** to find cafes with outdoor seating nearby - **Distance matrix** to calculate walking time from you to each cafe - **MapLibre GL JS** to plot everything ## Key code ### Geocoding + PlazaQL + distance matrix The core logic chains three API calls: geocode the address, query for nearby cafes, then get walking times to all of them in one batch. lib/plaza.js ``` import Plaza from "@plazafyi/sdk"; const client = new Plaza(); export async function geocode(address) { const result = await client.v1.geocode.forward({ q: address, limit: 1 }); const [lng, lat] = result.features[0].geometry.coordinates; return { lat, lng }; } export async function findCafes(lat, lng) { const query = `$$ = search(node, amenity: "cafe", outdoor_seating: "yes").around(distance: 800, geometry: point(${lat}, ${lng}));`; const result = await client.v1.query({ query }); return (result.features || []).map((f) => ({ id: f.properties?.osm_id, name: f.properties?.name || "Unnamed Cafe", lat: f.geometry.coordinates[1], lng: f.geometry.coordinates[0], cuisine: f.properties?.cuisine || null, })); } export async function getWalkTimes(originLat, originLng, cafes) { const result = await client.v1.matrix({ origins: [{ lat: originLat, lng: originLng }], destinations: cafes.map((c) => ({ lat: c.lat, lng: c.lng })), mode: "foot", }); return cafes.map((cafe, i) => ({ ...cafe, walkMinutes: Math.round(result.durations[0][i] / 60), distanceMeters: result.distances[0][i], })); } ``` ### Search flow in the React component The `App` component chains the three calls and passes results to the map and list: ``` const handleSearch = async (e) => { e.preventDefault(); setLoading(true); const location = await geocode(address); setCenter(location); const nearbyCafes = await findCafes(location.lat, location.lng); const cafesWithTimes = await getWalkTimes(location.lat, location.lng, nearbyCafes); setCafes(cafesWithTimes); setLoading(false); }; ``` ### Plotting markers on MapLibre Each cafe gets a marker. Clicking one flies the map to it: ``` cafes.forEach((cafe) => { const el = document.createElement("div"); el.textContent = "\u2615"; el.addEventListener("click", () => onSelect(cafe.id)); new maplibregl.Marker({ element: el }) .setLngLat([cafe.lng, cafe.lat]) .addTo(map); }); ``` ## How it works **Geocode** turns the address into `[lng, lat]` coordinates via `/v1/geocode`. **PlazaQL** finds every node tagged `amenity=cafe` + `outdoor_seating=yes` within 800 meters. The `.around()` filter handles the radius search, and the response comes back as GeoJSON. **Distance matrix** calculates walk times to all cafes in a single request — one origin (you), N destinations (the cafes). Much faster than routing to each one individually. **Sort and display** — combine cafe data with walk times, sort by shortest walk, render a clickable list alongside the map. ## Full source The complete app is at [github.com/plazafyi/example-apps/tree/main/cafe-finder](https://github.com/plazafyi/example-apps/tree/main/cafe-finder). Built with React + Vite + MapLibre GL JS. ## Variations to try **Filter by cuisine.** Add `cuisine: "coffee"` to the PlazaQL query to find proper coffee shops, or `cuisine: "italian"` for Italian cafes. **Expand the radius.** Change `distance: 800` to `distance: 1500` in the `.around()` call for a wider search. You’ll get more results but longer walk times. **Switch to cycling.** Change the matrix profile from `foot` to `bicycle` and rename “walk time” to “bike time”. Same code, different mode. **Filter by capacity.** Use an expression filter to only show cafes with seating capacity above a threshold: `search(node, amenity: "cafe", outdoor_seating: "yes").around(distance: 800, geometry: point(${lat}, ${lng})).filter(number(t["capacity"]) > 20)`. The `number()` cast is required because tag values are strings. **Look up by ID.** If you have a list of known cafe OSM IDs, fetch them directly: `search(node, id: [12345, 67890, 11111])`. **Add opening hours.** Many cafes in OSM have an `opening_hours` tag. Check `cafe.properties.opening_hours` to show whether each place is currently open. Parsing OSM opening hours is its own adventure — the [opening\_hours.js](https://github.com/opening-hours/opening_hours.js) library handles the format.