Search suggestions (places, brands and categories)

getSearchSuggestions() answers “what might the user mean?” in one call. A query like "italian" means both a cuisine category and Ristorante Italiano down the road, and the call returns both in one ranked list. A complete query works as well as a partial one.

Type a few letters into the demo below. Places, brands and categories come back in one list. Pick a brand or category to search it in view.

Basic usage

import { getSearchSuggestions } from '@tomtom-org/maps-sdk/services';
const { suggestions } = await getSearchSuggestions({ query: 'italian' });
for (const result of suggestions) {
if (result.kind === 'brand' || result.kind === 'category') {
console.log(`Refine by ${result.kind}: ${result.title}`);
} else {
console.log(result.title, result.subtitles);
}
}

Result kinds

Every result carries a kind. Narrowing on it tells you what else it carries.

KindWhat it isCarries
place, address, street, areaA locationid, title, subtitles, distanceInMeters, resolve()
brandA refinement, which narrows the next querytitle, refineParams, refine()
categoryA refinementThe same, plus category, its POICategory code
  • title is a POI’s name or an address’s first line. subtitles are the remaining address lines. They end with the country only when it differs from the one the results are in.
  • id is the place ID getPlaceDetails takes.

A location suggestion carries what a list needs, not the place. To get the full place with its geometry, call resolve() on the one the user picks:

const place = await result.resolve();
map.flyTo({ center: place.geometry.coordinates });

Acting on a refinement

refine() runs the follow-up search. A brand result searches that brand, and a category result searches that category. Both stay inside the original call’s scope (its filters.countries, filters.boundingBox or geoBias). It works whichever endpoint produced the result:

const { suggestions } = await getSearchSuggestions({ query: 'shell' });
const brand = suggestions.find((result) => result.kind === 'brand');
if (brand) {
const stations = await brand.refine();
}
  • refine() takes an optional AbortSignal. It doesn’t reuse the one given to getSearchSuggestions, because that call has settled by the time you act on a refinement.
  • refineParams holds the fuzzy search that refine() runs. You can adjust it and send it yourself:
const stations = await discoverPlaces({ ...brand.refineParams, limit: 50 });

Cost

  • By default, each call makes two requests, one per arm. Narrowing filters.kinds to only refinement kinds or only location kinds skips the other arm.
  • Call resolve() only on the pick, not on the whole list.
  • The call doesn’t debounce or cache. Do both per keystroke in your search box’s input loop.

Ranking

The SDK blends the list the way Places Search v3’s suggest does:

  • The list leads with at most one brand or category. It is offered only when the whole query names it from the start of a word. pizz offers Pizzeria, while baker st and amster offer none.
  • A brand must be backed by the places. One that only starts like the query is dropped unless a leading place carries it, so londo doesn’t offer London Fire Brigade. If filters.kinds leaves out place, no outlets come back to check, so the brand is offered as is.
  • Places follow in search order. When the query names a brand or category, places whose names contain the typed words move up, because the suggestion above them already stands for the category.
  • If you ask only for refinement kinds, the call returns every recognised refinement instead of the one leader.

Neither backend returns a score comparable with the other’s, so this blend is the SDK’s heuristic, not the API’s.

Parameters

Parameters follow Places Search v3’s suggest body. The full type is SearchSuggestionsParams. It also takes the options every service call takes.

ParameterTypeDescription
querystringWhat the user typed. Required.
limitnumberSize of the merged list. Default 5, capped at 10.
filters.kindsSearchSuggestionKind[]Which kinds of result to return, and so which arms run. Default: every kind.
filters.boundingBoxHasBBoxRestricts results to a box. Search arm only. geoBias then measures distances but does not rank the places. A refinement keeps the box.
filters.countriesstring[]Restricts results by country. Both arms.
filters.poiTypesPOICategory[]Restricts results by POI category. Search arm only.
filters.areaTypesGeographyType[]Restricts which administrative levels area results come from. Search arm only.
geoBias.positionHasLngLatThe point to search around. Ranks nearer results higher on both arms, unless a filters.boundingBox confines the search. distanceInMeters is measured from it.
geopoliticalViewGeopoliticalViewDefaults to the global one. See Geopolitical views.

The autocomplete arm accepts no category filter and no bounding box, so only the search arm gets them. A brand or category suggestion is therefore not category-filtered, because it is a concept, not a place.

Cancellation

Both arms share one signal, so aborting drops both. A slow arm can’t resolve on its own and refill the list under a newer query. To keep only the newest keystroke’s pair, use createLatestRequest:

import { createLatestRequest, getSearchSuggestions } from '@tomtom-org/maps-sdk/services';
const latestSuggestions = createLatestRequest();
searchBox.addEventListener('input', async () => {
const response = await latestSuggestions.run((signal) =>
getSearchSuggestions({ query: searchBox.value, signal })
);
if (response.current) showSuggestions(response.value.suggestions);
});

If either arm fails, the call rejects with an SDKError whose service is 'SearchSuggestions', instead of returning half a list.