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.
| Kind | What it is | Carries |
|---|---|---|
place, address, street, area | A location | id, title, subtitles, distanceInMeters, resolve() |
brand | A refinement, which narrows the next query | title, refineParams, refine() |
category | A refinement | The same, plus category, its POICategory code |
titleis a POI’s name or an address’s first line.subtitlesare the remaining address lines. They end with the country only when it differs from the one the results are in.idis the place IDgetPlaceDetailstakes.
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 optionalAbortSignal. It doesn’t reuse the one given togetSearchSuggestions, because that call has settled by the time you act on a refinement.refineParamsholds the fuzzy search thatrefine()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.kindsto 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.
pizzoffers Pizzeria, whilebaker standamsteroffer 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
londodoesn’t offer London Fire Brigade. Iffilters.kindsleaves outplace, 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.
| Parameter | Type | Description |
|---|---|---|
query | string | What the user typed. Required. |
limit | number | Size of the merged list. Default 5, capped at 10. |
filters.kinds | SearchSuggestionKind[] | Which kinds of result to return, and so which arms run. Default: every kind. |
filters.boundingBox | HasBBox | Restricts results to a box. Search arm only. geoBias then measures distances but does not rank the places. A refinement keeps the box. |
filters.countries | string[] | Restricts results by country. Both arms. |
filters.poiTypes | POICategory[] | Restricts results by POI category. Search arm only. |
filters.areaTypes | GeographyType[] | Restricts which administrative levels area results come from. Search arm only. |
geoBias.position | HasLngLat | The point to search around. Ranks nearer results higher on both arms, unless a filters.boundingBox confines the search. distanceInMeters is measured from it. |
geopoliticalView | GeopoliticalView | Defaults to the global one. See Geopolitical views. |
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.
Related guides and examples
- Discover places — where a brand or category refinement’s follow-up runs
- POI categories — the category vocabulary behind
categoryresults - Search playground —
getSearchSuggestions()behind a search box