Navvo
API Reference

Search

Place search for Jordan and the region — Arabic, English and Arabizi queries, typo-tolerant ranking, filters, facets, highlights and optional meaning-based matching.

Search

Free-text search over Navvo's place catalogue. One query box handles Arabic, English, Arabizi (Arabic written in Latin letters and digits, such as saydaliyeh or mat3am) and mixed queries. Navvo recognises category words (pharmacy, صيدلية), areas (cafe in Abdoun), "near me" and "open now" phrases, tolerates typos, and ranks by how well a place's name matches before it weighs distance and popularity.

Query understanding is deterministic: it is a set of rules and curated vocabularies, not an AI model, so the same query always reads the same way and never waits on a model. Everything runs on Navvo's own infrastructure.

Typo tolerance covers short words too. Take an English word of four to six letters that no place name uses but that is one letter away from a word place names do use. Navvo also searches its corrected form next to the query as typed, and ranks the two result sets together: cafr finds cafés and hotl finds hotels. The correction comes from the words in Navvo's own place names and favours the commonest typing slips, such as a neighbouring key or two swapped letters. Arabizi words are never corrected this way, because they are Arabic written in Latin letters, not misspellings.

Scope required: search

EndpointUse it for
GET /searchThe results list.
GET /search/suggestionsAutocomplete suggestions while the user types.
GET /search/similar"More like this" on a place page.
POST /search/eventsReporting which result the user opened, with their consent.
Updated 2026-09-30. Search has a new query pipeline. Every existing response field keeps its name and type, and everything new is additive. Four behaviours changed and are worth checking against your integration:
  • locale is now honoured and echoed back. It used to be dropped, so every response said en.
  • lat/lon are range-checked, and a coordinate off the globe returns 400. q is truncated to 200 characters.
  • category includes sub-categories: category=food-and-drink returns cafés, restaurants and bakeries.
  • understanding comes from the deterministic parser and is always available. Its old model-generated contents are gone.
The ranking described on this page has been live since 30 September 2026, and meaning-based matching since 1 October 2026.What records Navvo keeps when you search, and how consent works, is covered in Search analytics & privacy.

Search for places by free-text query.

curl -s "https://navvo.io/api/v1/search?q=coffee&lat=31.9539&lon=35.9106&limit=10" \
  -H "x-api-key: nvvo_YOUR_KEY"

Query parameters

q
string required
The search query, as the user typed it: coffee in Abdoun, صيدلية مناوبة قريبة, saydaliyeh, Abdali Mall. It must not be blank. Anything past 200 characters is ignored.
locale
string
The user's language. Send en or ar, or a full language tag such as ar-JO or en-US (up to 35 characters). Tags that start with ar resolve to ar and everything else to en, and the response echoes the resolved value. locale never filters results: Arabic and English names both match whichever you send.
page
number
1-based page number. Defaults to 1; values below 1 are raised to 1.
limit
number
Items per page. Defaults to 25 and is clamped to 1–100. Out-of-range values are clamped, not rejected.
bbox
string
"Search this area": west,south,east,north. Only places inside the box are returned, and the response echoes bbox. A malformed value is ignored.
lat
number
Latitude of the ranking origin, from −90 to 90. Send it together with lon. Nearer places rank higher within the same match band (see Ranking), but the origin does not filter anything unless you also send radiusKm. The response echoes near: { lat, lng }. A non-numeric or out-of-range value returns 400.
lon
number
Longitude of the ranking origin, from −180 to 180. lng is accepted as an alias, and sending both with different values returns 400.
lng
number
Alias for lon.
originType
string
What the origin is. device (default) means the user's own position. map means the centre of the map they are looking at: distance then counts for less and fades over a wider radius, and items carry no distanceKm. Send map when you bias a plain-text search toward the viewport rather than toward the user.
category
string
Only return places in this category or any of its sub-categories. category=food-and-drink covers cafés, restaurants and bakeries; category=pharmacy covers pharmacies only. Send several slugs separated by commas to match any of them: category=cafe,coffee-shop (up to 10). Slugs come from GET /places/categories; a value that is not a category matches nothing. The response echoes category.
mode
string
submit (default) for a search the user asked for, e.g. by pressing Enter. typeahead for requests fired while they type: prefix matches count as strong, popular places weigh more (the user is probably typing a well-known name), and meaning-based matching never runs. It also tells Navvo's analytics which requests were keystrokes.
semantic
string
Meaning-based matching: auto (default), on or off. See Meaning-based matching. With an API key, auto behaves like off, so send on to opt in.
facets
string
Comma-separated facet names to count: category, amenities, or both. Unknown names are ignored. See Facets.
highlight
boolean
When true, items whose name matches the query carry a highlight object. See Highlights.
openNow
boolean
When true, only places that are open right now are returned. This is evaluated in Jordan time and accounts for public holidays. Places without opening-hours data are left out.
minRating
number
Only places rated at least this much, 0–5.
price
string
Price levels to keep, comma-separated (1,2): 1 up to JOD 5 per person, 2 JOD 5–10, 3 JOD 10–20, 4 more. A place matches when the midpoint of its listed price range, converted to dinars, falls in one of the levels. Places without a listed price are left out, and only a few thousand places list one (mostly cafés and restaurants), so a price filter returns far fewer results.
sort
string
relevance (default), distance (nearest first; needs lat/lon) or rating (highest first). distance and rating replace the relevance order entirely.
radiusKm
number
Only places within this many kilometres of lat/lon, from 0.1 to 500. It needs an origin.
amenities
string
Comma-separated amenity slugs. A place matches only if it has all of them, for example wheelchair-accessible-entrance,delivery. They are the same option slugs as GET /places/facets → options[].slug, or you can take them from facets=amenities.
analytics
boolean
Send false and Navvo's search analytics keep no record of this search. It is the same as the X-Navvo-Analytics: off header, and the right setting for tests, health checks and bulk jobs.
Boolean parameters (highlight, openNow, analytics) read true or 1 as true. Any other value, including false, 0 or yes, counts as false. Because analytics defaults to on, any value other than true/1 switches it off.

Request headers

These headers are all optional. The first four control what Navvo records; Search analytics & privacy explains the model behind them.

X-Navvo-Session
string
A random id for the user's current session, generated by you and sent only after the user has agreed to let their activity improve search. With it, the clicks you report through POST /search/events can be linked to this search, and a burst of keystroke searches counts as one. Format: 16–64 characters from A–Z a–z 0–9 _ -, with no run of six or more digits. Any other value is ignored. Never derive it from a user id, email address, phone number or device identifier. Navvo stores only a keyed hash of it computed together with the date, so hashes from different days can't be linked.
X-Navvo-Analytics
string
off keeps this search out of Navvo's search analytics, the same as analytics=false.
Sec-GPC
string
1 means the user's browser asks not to be tracked (Global Privacy Control). Navvo then ignores X-Navvo-Session and rejects events for this search. The search record is still kept, but without a session link.
DNT
string
1 is treated exactly like Sec-GPC: 1. If you proxy searches through your own server, forward both headers from your user's request.
X-Navvo-Source
string
An optional traffic label. sdk marks requests from a client SDK built on Navvo; other values are reserved for Navvo's own apps. API-key requests without sdk are reported as api. Navvo's own SDKs send sdk automatically: Web and Node from 0.2.1, Python, Go and Java from 0.2.0. If the browser SDK reaches the API through a proxy of your own, allow this request header in the proxy's CORS policy.

Response

FieldTypeDescription
querystringYour query, echoed.
localestringThe resolved locale: en or ar.
queryIdstringA 24-character id for this result list. Pass it to POST /search/events when the user opens a result. It is opaque: it contains no query text or user data, and a new one is issued on every request.
understandingobjectHow the query was read. See Query understanding.
places.items[]arrayThe ranked results (see below). Empty when nothing matches: Navvo never pads the list with unrelated places.
places.totalnumberHow many places match. On the index this is an estimate, capped at 1,000, so treat it as "about N results". It is 0 only on a genuine zero-match.
places.page / places.limitnumberPagination echo.
facetsobjectOnly when you asked for facets. See Facets.
suggestions[]arrayOnly when places.items is empty: popular places near the origin to offer as "no results — try these". Same shape as places.items[]. See No results.
didYouMeanstringOnly on a zero-match that looks like a misspelling: a corrected query taken from Navvo's vetted vocabulary. Plain text.
bbox / category / near—Echo of the bbox, category and origin you sent.
indexobjectSearch index status: { provider, configured, available, mode }. See Index status & monitoring.

Every response carries Cache-Control: no-store. Each response has a fresh queryId, so a cached copy would tie the user's clicks to someone else's search. Don't cache /search responses in a service worker or CDN.

Each places.items[] element:

FieldTypeDescription
idstringPlace id.
slugstringURL slug, for GET /places/{slug}.
nameEn / nameArstring | nullNames.
nameEnTransliteratedbooleanPresent, as true, only when nameEn was produced by spelling the Arabic name in Latin letters because no English name is known (about 35,000 places). Show nameAr first for these, and don't present nameEn as an official name.
categoriesstringCanonical category slugs, primary category first.
canonicalCategorySlugstring | nullPrimary category.
rawCategoriesstringOriginal, un-normalised categories.
ratingnumberRating, or 0 if there is none.
sponsoredbooleanA sponsored listing, or a paid placement from a Navvo rule.
promotedbooleanPresent, as true, only when a Navvo editorial or sponsored rule placed this result. Label it in your UI (for example "Promoted").
centerstringLocation as WKT POINT(<lng> <lat>), longitude first.
lat / lngnumber | nullThe same coordinates as numbers, or null when center is missing.
iconLightUrl / iconDarkUrlstring | nullCategory pin icons, as absolute URLs. See Places → Category marker icons.
imageUrlstring | nullThe place's picture, as an absolute URL, or null when there is none. See Places → Place image.
distanceKmnumberStraight-line distance from lat/lon, in kilometres. Present only with a device origin (originType=device, the default).
highlightobjectWith highlight=true: { nameEn?, nameAr? }. See Highlights.
reviewCountnumberHow many reviews rating is based on. Present when known.
categoryNamestringThe primary category's display name in the response locale, such as Coffee Shop or مقهى.
priceRangestringThe usual spend per person as listed, such as JOD 5–10. Present when known.
priceLevelnumber1–4, the level price filters on, worked out from priceRange (like $ to $$$$). Present when priceRange can be read.
addressstringA short street address for one line, in the response locale's script when the place has one, such as Al-Mutanabbi St. 22.
areastringThe neighbourhood or district, such as Zahran.
hoursobjectToday's opening state in Jordan time. See Result details. Absent when the place has no opening hours.
highlightsstringUp to three things the place is known for, such as Great coffee or Rooftop seating. In English.
{
  "query": "coffee",
  "locale": "en",
  "queryId": "AaDxmIYgiEC1I4ZdHCWiERmR",
  "understanding": {
    "unavailable": false,
    "purpose": "search_understanding",
    "parsed": { "language": "en", "intent": "place", "category": "Coffee Shop", "categories": ["coffee-shop", "cafe", "coffee-stand", "cafeteria", "cat-cafe", "tea-house"] }
  },
  "places": {
    "items": [
      {
        "id": "e3bacbd9-1e5d-474c-b84e-2fe7e1556541",
        "slug": "coffee-factory-co-jabal-amman-a3ddbd",
        "nameEn": "Coffee Factory & Co. - Jabal Amman",
        "nameAr": null,
        "categories": ["coffee-shop"],
        "canonicalCategorySlug": "coffee-shop",
        "rawCategories": ["Coffee shop"],
        "rating": 4.6,
        "sponsored": false,
        "center": "POINT(35.905152 31.952061)",
        "lat": 31.952061,
        "lng": 35.905152,
        "iconLightUrl": "https://navvo.io/uploads/category-icons/coffee-shop-light.svg",
        "iconDarkUrl": "https://navvo.io/uploads/category-icons/coffee-shop-dark.svg",
        "imageUrl": "https://navvo.io/uploads/place-images/e3/e3bacbd9-1e5d-474c-b84e-2fe7e1556541-dc3949fa.webp",
        "distanceKm": 0.55
      }
    ],
    "total": 1000,
    "page": 1,
    "limit": 10
  },
  "near": { "lat": 31.9539, "lng": 35.9106 },
  "index": { "provider": "navvo-search", "configured": true, "available": true, "mode": "keyword" }
}

Query understanding

understanding tells you how Navvo read the query. The results already reflect it, so you don't have to act on it. Use it for interface hints instead, such as showing an "Open now" chip when the user typed "open now". A set of rules produces it with no model involved, so identical queries always read identically and unavailable is always false. The field is kept for compatibility.

Everything useful sits under understanding.parsed:

FieldTypeDescription
languagestringar, en, arabizi, mixed or unknown.
intentstringcategory: the whole query names a category (pharmacy, صيدلية قريبة). food: a dish or type of food (shawarma, كنافة). place: anything else, such as a name, a mix, or a category word inside a longer query.
categorystringDisplay name of the main category the query refers to. It also appears when a category word sits inside a longer query.
categoriesstringUp to 6 category slugs the query matched.
nearstring"me" when the query asks for nearby places (near me, قريب مني, 2reeb).
openNowbooleantrue when the query asks for places that are open (open now, مفتوح).
localitystringAn area named in the query, such as Abdoun in cafe in Abdoun.
qualifierstringbest, cheap, open24 (open around the clock), onDuty (on-duty pharmacies, مناوبة) or lateNight.

Keys appear only when they apply. For cafe in Abdoun:

"parsed": { "language": "en", "intent": "category", "category": "Coffee Shop", "categories": ["coffee-shop", "cafe", "coffee-stand", "cafeteria", "cat-cafe", "tea-house"], "locality": "Abdoun" }
Arabizi and transliteration are handled for you. A query like saydaliyeh is also tried as Arabic script, and an Arabic query is also tried in Latin letters, because many places in the catalogue have transliterated English names. You never need to transliterate before calling.

Facets

facets=category,amenities adds counts you can turn into filter chips:

"facets": {
  "category": [
    { "slug": "coffee-shop", "name": { "en": "Coffee Shop", "ar": "مقهى" }, "count": 812 },
    { "slug": "cafe", "name": { "en": "Cafe", "ar": "مقهى" }, "count": 108 },
    { "slug": "coffee-roastery", "name": { "en": "Coffee Roastery", "ar": "محمصة قهوة" }, "count": 10 }
  ],
  "amenities": [
    { "slug": "wheelchair-accessible-entrance", "count": 301 },
    { "slug": "delivery", "count": 188 }
  ]
}
  • Each list is sorted by count, highest first. A category's name.en or name.ar can be null.
  • Counts cover places that match the query's words and your filters. items can also include nearby or meaning-based results, so the counts guide a filter UI rather than break down the list exactly.
  • Pass a slug straight back as category= or amenities= to apply the filter.
  • facets is absent when you didn't ask for it, and can be absent when the fallback answered (index.available: false).

Highlights

With highlight=true, an item whose name matches the query carries highlight.nameEn and/or highlight.nameAr. Each is the name, HTML-escaped, with the matched words wrapped in <em>…</em>. A key is present only when that name contains a match:

"highlight": { "nameEn": "<em>Coffee</em> Factory &amp; Co. - Jabal Amman" }

The string is safe to insert as HTML. The most robust approach, though, is to render it as text nodes: split on the tags and decode the entities yourself, and never pass it to innerHTML or dangerouslySetInnerHTML:

// "<em>Coffee</em> Factory &amp; Co." -> [{ text: "Coffee", match: true }, { text: " Factory & Co.", match: false }]
function highlightSegments(html) {
  const decode = (s) => s
    .replace(/&lt;/g, "<").replace(/&gt;/g, ">")
    .replace(/&quot;/g, '"').replace(/&#39;/g, "'")
    .replace(/&amp;/g, "&");
  return html.split(/(<em>.*?<\/em>)/g).filter(Boolean).map((part) => {
    const m = part.match(/^<em>(.*)<\/em>$/);
    return { text: decode(m ? m[1] : part), match: Boolean(m) };
  });
}

didYouMean and every field of GET /search/suggestions are plain text and carry no markup.

Result details

An item can carry what a results list or a map card shows: reviewCount, categoryName, priceRange, priceLevel, address, area, hours and highlights. Each comes from the place record and is left out when the place has no such data, so check that a field is there before you use it. The same fields appear on zero-result suggestions and on similar places.

hours describes today in Jordan time. It is worked out when the search runs, so don't cache a response for more than a few minutes.

FieldTypeDescription
statestringopen, closing (closes within 45 minutes) or closed.
alwaysOpenbooleantrue when the place is open around the clock: every day, or at least for the next 24 hours (several 24-hour days in a row). There is no closesAt then.
closesAtstringHH:MM: when an open place closes.
opensAtstringHH:MM: when a closed place next opens, within the coming 7 days.
opensDaystringThe weekday of that opening, sunday to saturday, when it is not today.
hoursMayVarybooleantrue during a public holiday in the place's country: today's hours may differ.
"hours": { "state": "closed", "opensAt": "09:00", "opensDay": "sunday" }

A place that closes at 2 a.m. is still open after midnight, with "closesAt": "02:00".

Ranking

Every candidate first lands in a match band. Bands never mix, so a better band always ranks above a worse one:

  1. Exact. The query is the place's name or one of its alternative names, including after a spelling variant (an Arabizi query converted to Arabic, for example).
  2. Strong. The name contains every word of the query spelled correctly, or, with mode=typeahead, starts with what the user typed.
  3. Complete. Every word matches, allowing for typos, or the words match across the name, category and neighbourhood (cafe abdoun finds a café in Abdoun), or the place belongs to the category the query asks for.
  4. Partial. Only some of the words match.
  5. Meaning-based. The place was found only by meaning-based matching.

Within a band, Navvo combines distance from the origin, popularity (review volume and map prominence), text relevance, typos, and editorial boosts. A few rules shape that order:

  • Category searches level the field. For pharmacy or صيدلية قريبة, a place literally named "Pharmacy" gets no head start. Nearby, popular places in the category lead.
  • Areas move the origin. In cafe in Abdoun, results rank around Abdoun rather than around lat/lon. Without a connecting word (cafe abdoun), your origin stays and places in Abdoun get a boost. If the whole query is an area, Navvo searches for it by name.
  • Supporting features step back. Bus stops, car parks, taxi stands, gates, entrances and ATMs rank below the main place unless the query asks for them.
  • Jordan first. Without an origin, places in Jordan rank ahead of equally good matches abroad.
  • A far branch doesn't outrank nearby ones. With an origin, an exact name match more than 25 km away competes on distance with nearer places whose names contain the query, so carrefour from Amman lists Amman's branches first. A major place keeps its lead however far it is (Petra, Al-Aqsa Mosque).
  • "Open now" in the query demotes places known to be closed (open first, then unknown hours, then closed) even without openNow=true. openNow=true removes everything that isn't open.
  • Sponsored places that are relevant to the query get a small boost within their band. They never jump to a better band. Results pinned by a Navvo rule carry promoted: true, and also sponsored: true when the placement is paid.

sort=distance and sort=rating replace this order entirely.

Meaning-based matching

Keyword matching finds places whose names and categories contain the query's words. Meaning-based matching also finds places that fit a description, such as quiet place to study or مكان للعشاء مع العائلة, even when no word matches, including across Arabic and English. It runs entirely on Navvo's own infrastructure, so query text never goes to a third party.

  • It is only a fallback. It adds results only when keyword matching finds few strong matches. A query that already has good name matches is not changed, and meaning-based results always rank below keyword results (band 5).
  • It needs some evidence in the query: at least one word must match the catalogue (typos allowed), or the query must name a category or carry a filter. Random letters and off-topic questions get no meaning-based results.
  • It needs a query of at least two complete words, mode=submit, and a query that isn't a pure category search. It never runs for typeahead.
  • Navvo waits at most 350 ms for it. If it is slower or unavailable, you get the keyword results without it and no error.
  • semantic=off disables it. semantic=on allows it on API-key requests, where auto behaves like off.
  • index.mode on the response is hybrid when meaning-based matching contributed to that response, and keyword otherwise.

No results

When nothing matches, places.items is empty and places.total is 0. Navvo never fills the list with unrelated places. Instead:

  • suggestions[] lists popular places near the origin, or popular places in Jordan when there is no origin, in the same shape as the items. Show them under a heading such as "No results — popular places", not as results.
  • didYouMean appears when the query looks like a misspelling of something in Navvo's vetted vocabulary (category names and well-known place names). Offer it as a link that re-runs the search.

Pagination

page and limit page through one ranked list. Each query ranks at most its top 200 candidates, so stop when a page comes back with fewer than limit items. Don't compute the last page from places.total, which is an estimate.

To fetch a place's full details (hours, photos, entrances), call GET /places/{slug} with the slug from a result.

Errors

StatusWhen
400q is missing or blank; a parameter is outside its allowed values or range (originType, mode, semantic, sort, minRating, radiusKm, a locale longer than 35 characters, lat outside −90…90, lon/lng outside −180…180); lon and lng were both sent with different values; or sort=distance or radiusKm was sent without lat/lon.
401 / 403The key is missing or invalid, lacks the search scope, or is over quota (QUOTA_EXCEEDED).
429Too many requests from one client IP (RATE_LIMITED).

If the search index is unreachable, /search doesn't fail. A simpler database fallback answers instead, and index.available is false. Error bodies follow the standard shape in Conventions.


Metering & rate limits

Each search request counts as one request against your key's per-minute and per-day quota, and as weighted requests for your plan's billing (see plans & metering). An unlimited key has no quota, but its requests are still weighted:

EndpointWeighted requestsLimits
GET /search2Your key's per-minute and per-day quota (Rate limits).
GET /search/suggestions1Your key's quota.
GET /search/similar2Your key's quota.
POST /search/events0 (not metered)60 requests per minute per client IP (not for an unlimited key); up to 20 events and 16 KB per request.

Meaning-based matching does not change the weight of a request. Requests without an API key get meaning-based matching within an approximate hourly budget per client IP address: about 120 searches in which meaning-based matching actually runs. Beyond it they silently use keyword matching only.

While the user types, debounce (about 250 ms), send mode=typeahead, and cancel the previous request when a new keystroke arrives. On Enter, send one mode=submit request. When the user picks a place from the list, open it directly rather than firing another search.

Index status & monitoring

Every response carries an index object:

FieldTypeDescription
providerstringAlways navvo-search.
configuredbooleanWhether the search index is configured on this deployment.
availablebooleanWhether the index answered this request. false means a simpler database fallback answered: results still come back, but typo tolerance, facets and meaning-based matching are unavailable.
modestringhybrid when meaning-based matching contributed to this response, keyword otherwise.
Breaking for monitors (2026-09-30).GET /api/v1/health now reports search.engine as navvo-search when the full index is answering, or fallback when the degraded database fallback is. Earlier responses used engine-specific labels. If a monitor or alert compares this string, update it to the new values. search.configured is unchanged.
Copyright © 2026