Build a store locator
Build a store locator
A classic integration: let users search for places, list results with pictures and open/closed status, and show full details when they pick one. Uses Search, Places, and a map.
Scopes required: search, places (and tiles for the map).
1. Search as the user types
Call GET /search with the query. Debounce input to stay within rate limits.
async function search(q) {
const res = await fetch(
"https://navvo.io/api/v1/search?" + new URLSearchParams({ q, limit: "10" }),
{ headers: { "x-api-key": KEY } }
);
const data = await res.json();
return data.places.items; // [{ slug, nameEn, nameAr, rating, lat, lng, imageUrl, hours, … }]
}
2. Render results with pictures & status
Each result already carries what a results row shows, so you don't need another request per result. It has the picture (imageUrl), rating and reviewCount, categoryName, priceRange, address and area, plus today's opening state in hours. A field is left out when the place has no data for it, so check that it's there before you use it. See Search → Result details.
function hoursText(h) {
if (!h) return ""; // no opening hours on record
if (h.alwaysOpen) return "Open 24 hours";
if (h.state === "closed") return ["Closed · Opens", h.opensDay, h.opensAt].filter(Boolean).join(" ");
return h.closesAt ? `${h.state === "closing" ? "Closing soon" : "Open"} · Closes ${h.closesAt}` : "Open";
}
function resultRow(item) {
return {
name: item.nameEn || item.nameAr,
picture: item.imageUrl, // null when the place has none
rating: item.rating > 0 ? `${item.rating.toFixed(1)} (${item.reviewCount ?? 0})` : "",
subtitle: [item.categoryName, item.priceRange].filter(Boolean).join(" · "),
address: [item.address, item.area].filter(Boolean).join(", "),
hours: hoursText(item.hours),
};
}
Plot each result on the map at its lng / lat. To let users narrow the list, add openNow=true, or price with the levels to keep: price=1,2 keeps places up to JOD 10 per person. A place without a listed price never matches a price filter.
If you have a slug but no search result (from a saved list, for example), GET /places/thumb returns that place's thumbnail and open state.
With the Web SDK
@navvo/maps-js 0.3.0 returns the same fields, typed. It parses each position into lngLat and takes price as a list:
import { NavvoServices } from "@navvo/maps-js";
const services = new NavvoServices({ apiKey: KEY });
const r = await services.search("cafe", { near: [35.9106, 31.9539], price: [1, 2], mode: "submit" });
const rows = r.items.map(resultRow);
On a page that loads the SDK with a <script> tag (see Display a map), create the client with new NavvoMaps.NavvoServices({ apiKey: KEY }).
3. Show full detail on selection
When the user picks a result, load the full record from GET /places/{slug}:
async function detail(slug) {
const res = await fetch(`https://navvo.io/api/v1/places/${encodeURIComponent(slug)}`, {
headers: { "x-api-key": KEY },
});
const place = await res.json();
if (!place) return null; // unknown slug returns 200 with null body
return place; // includes reviews, photos, questions, arrivalIntelligence
}
Display nameEn/nameAr, categories, contact, openingHours, rating, photos, and reviews.
4. Send users to the right door
place.arrivalIntelligence gives you the recommended entrance — better than the center pin for directions:
const target = place.arrivalIntelligence?.candidates?.[0]?.routeTarget; // { lat, lng }
const directionsUrl =
`https://navvo.io/api/v1/routing/route`; // POST with that target — see Directions guide
arrivalIntelligence.candidates[0].routeTarget (the recommended entrance) as the routing destination instead of center. For a delivery app, also show lastMetersGuidance to the driver.Putting it together
Search
GET /search?q=… → results that already carry a picture, rating, price, address and today's hours.
Detail
GET /places/{slug} on selection → full record + arrival entrance.
Route
POST /routing/route (or /traffic/route) to the recommended entrance.
Search analytics & privacy
Every GET /search call leaves a scrubbed, pseudonymous record that Navvo keeps to improve search, for 30 days by default (90 if your project chooses, or none if you switch its search analytics off). A store locator should get three things right:
- Mark keystrokes. Send
mode=typeaheadwhile the user types andmode=submitwhen they press Enter. A burst of keystrokes then counts as one search, and typeahead stays fast because meaning-based matching never runs for it. - Report what users pick, but only with consent. If your app offers an "Improve search with my activity" setting and the user turns it on, send a random per-session
X-Navvo-Sessionid with each search. Then report the result they open withPOST /search/events:click/open_place, followed bydirectionswhen they route there. - Opt out where records don't belong. Add
analytics=falseto automated tests and bulk jobs.
What is recorded, how long it is kept and how sensitive searches are handled is all covered in Search analytics & privacy.