Navvo
Guides

Build a store locator

Search places, show rich details, and guide users to the right entrance.

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
Use 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

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=typeahead while the user types and mode=submit when 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-Session id with each search. Then report the result they open with POST /search/events: click / open_place, followed by directions when they route there.
  • Opt out where records don't belong. Add analytics=false to 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.

Copyright © 2026