Discover places (fuzzy/geometry/along-route)

discoverPlaces() is the one search function. Its parameters pick which of three searches runs:

SearchRuns whenFinds
Along-routeroute is givenPlaces within a detour budget of a route
Geometrygeometries is givenPlaces inside circles or polygons
Fuzzyneither isPlaces and addresses for free text, optionally near a geoBias

There is no separate export per search. Each returns places as a GeoJSON FeatureCollection that the Places module shows as it is.

Available search types

Fuzzy search tolerates typos, partial input and natural-language phrasing, for POIs and addresses alike:

import { discoverPlaces } from '@tomtom-org/maps-sdk/services';
const results = await discoverPlaces({
query: 'coffe shop near dam square',
geoBias: { position: [4.9041, 52.3676] },
});
// A query string on its own runs a fuzzy search, the same as `{ query }`
const museums = await discoverPlaces('museums in amsterdam');

Only fuzzy search takes geoBias, cursor, typeahead, minFuzzyLevel, maxFuzzyLevel and filters.countries. See Fuzzy search parameters.

Geometry search finds places inside one or more areas:

import { discoverPlaces } from '@tomtom-org/maps-sdk/services';
const inCircle = await discoverPlaces({
query: 'restaurants',
geometries: [{ type: 'Circle', coordinates: [4.9041, 52.3676], radius: 5000 }], // radius in metres
});
const inPolygon = await discoverPlaces({
query: 'museums',
geometries: [{
type: 'Polygon',
coordinates: [[[4.7, 52.2], [5.1, 52.2], [5.1, 52.5], [4.7, 52.5], [4.7, 52.2]]],
}],
});
  • geometries takes a Circle (coordinates plus radius in metres, an SDK extension to GeoJSON), a Polygon, a MultiPolygon or a polygon FeatureCollection such as a geometryData result, passed as it is. You can mix several in one array.
  • Only a polygon’s outer ring is sent to the API, so holes are ignored. The ring is sampled down to 50 vertices, and a MultiPolygon of more than 50 polygons keeps its 50 largest.

Along-route search finds places reachable within maxDetourTimeSeconds of a route, such as fuel stops, cafés or EV chargers:

import { calculateRoute, discoverPlaces } from '@tomtom-org/maps-sdk/services';
const routes = await calculateRoute({ locations: [[4.9041, 52.3676], [5.1214, 52.0907]] });
const chargers = await discoverPlaces({
route: routes.features[0],
maxDetourTimeSeconds: 600,
filters: { poiCategories: ['CHARGING_LOCATION'] },
limit: 10,
});
const cafes = await discoverPlaces({
route: [[4.9, 52.37], [4.95, 52.28], [5.1, 52.09]], // or a GeoJSON LineString
maxDetourTimeSeconds: 300,
query: 'cafe',
sortBy: 'detourOffset',
});

Give it a query, a filter or both, as validation rejects an along-route search with neither. indexes narrows a search but doesn’t count as a filter here.

Along-route results carry detourTimeInSeconds and detourDistanceInMeters, and either can be negative. A negative detour means the route through the place is faster or shorter than the original one. Results sorted by 'detourOffset' also carry detourOffsetInMeters, the distance from the start of the route to where the detour leaves it. The properties type is AlongRoutePlaceProps.

const minutes = Math.round(chargers.features[0].properties.detourTimeInSeconds / 60);

The Node.js along-route search example finds fast chargers within a one-minute detour of a route and prints each detour.

Search as you type

For a search box, use getSearchSuggestions(). One call returns places with brand and category suggestions.

Parameter reference

The full type is DiscoverPlacesParams. An option of another search variant, such as cursor beside geometries or geoBias beside route, is a compile error. It isn’t silently dropped.

Common parameters

All three searches take these, plus the options every service call takes (language, signal, validateRequest), covered in Request options and errors:

ParameterTypeDescription
querystring (optional)Search text, as plain text. The SDK encodes it, so a value you already encoded is encoded again and can resolve to a different place. Omit it to search by filters or geometry alone.
limitnumberMaximum results, up to 100. Default 10.
filtersPlaceFilters (SearchFilters on fuzzy search)What may come back. See Filters.
openingHoursOpeningHoursModeAdds opening hours to POIs, e.g. 'nextSevenDays'.
timeZoneTimeZoneRequest'iana' adds the POI’s IANA time zone, e.g. 'Europe/Amsterdam'.
relatedPoisRelatedPoisRequest'off' (default), 'child', 'parent' or 'all'.
geopoliticalViewGeopoliticalViewWhich country a place in a disputed territory belongs to. Defaults to the global one. See Geopolitical views.
mapcodesMapcodeType[]Adds mapcodes: 'Local', 'International', 'Alternative'.
extendedPostalCodesForSearchIndexType[]Indexes to return extended postal codes for. Default: all except 'Geo'.

Filters

Every option that drops results lives under filters. geoBias stays separate. It sets where to look, not what to accept.

MemberTypeDescription
poiCategoriesPOICategory[]POI category codes, found through POI categories.
poiCategoryQuerystring | string[]Categories in words, discoverPlaces only, never with poiCategories. See Naming the category in words.
poiBrandsstring[]Brand names, case-sensitive, e.g. ['Starbucks', 'Costa Coffee']. Quote a name with a comma, as in '"A,B"'.
indexesSearchIndexType[]Which indexes to search: 'Geo', 'PAD', 'Addr', 'Str', 'XStr', 'POI'. Default: all.
geographyTypesGeographyType[]Kinds of Geography result, e.g. ['Municipality', 'Neighbourhood'].
connectorsConnectorType[]EV connector types, e.g. ['IEC62196Type2CCS', 'Chademo'].
fuelTypesFuel[]Fuel types, e.g. ['Petrol', 'Diesel', 'E85'].
minPowerKW / maxPowerKWnumberEV charging power bounds, in kilowatts.
countriesstring[]ISO 3166-1 alpha-2 or alpha-3 codes, e.g. ['NL', 'DE']. Fuzzy search only.
  • A filter excludes. A place that doesn’t match is dropped, not ranked lower.
  • Values within one member are OR’d, and members are AND’d. poiBrands: ['Shell', 'BP'] with fuelTypes: ['Diesel'] keeps stations of either brand that sell diesel.
  • The type follows the search. Fuzzy search takes SearchFilters (PlaceFilters & GeographicFilters). Geometry and along-route search take PlaceFilters, so filters.countries beside geometries or route is a compile error. Geocoding takes countries and geographyTypes.
import { discoverPlaces } from '@tomtom-org/maps-sdk/services';
import type { SearchFilters } from '@tomtom-org/maps-sdk/services';
const dieselStations: SearchFilters = {
countries: ['NL', 'BE'],
poiBrands: ['Shell', 'BP'],
fuelTypes: ['Diesel'],
};
const results = await discoverPlaces({
filters: dieselStations,
geoBias: { position: [4.9041, 52.3676], radiusMeters: 20000 },
});

In the demo below, the same fuel station search runs once with a brand, once with a fuel type and once with a country filter, each result set in its own colour:

Geographic bias

Fuzzy search and geocoding take one geoBias, either a point or a rectangle. The compiler rejects both together, and a call that passes both anyway rejects with an INVALID_REQUEST error.

// Around a point: ranks nearer results higher; with radiusMeters, also confines results to the circle
geoBias: { position: [4.9041, 52.3676], radiusMeters: 2000 }
// Inside a rectangle: [west, south, east, north], or any GeoJSON a bbox derives from
geoBias: { boundingBox: map.getBBox() }
  • position takes [longitude, latitude] or any GeoJSON carrying one.
  • Omit geoBias to search without a geographic bias.
  • Geometry and along-route search take none, because their geometries or route is the search area.

Fuzzy search parameters

On top of the common parameters:

ParameterTypeDescription
geoBiasGeoBias{ position, radiusMeters? } or { boundingBox }. See Geographic bias.
typeaheadbooleanTreat the query as partial input. Default false.
cursorstringThe previous response’s nextCursor, to fetch the next page. See Pagination.
minFuzzyLevelFuzzyLevelMinimum fuzziness, 1 to 4. Default 1. Can’t exceed maxFuzzyLevel.
maxFuzzyLevelFuzzyLevelMaximum fuzziness, 1 to 4. Default 2. Higher tolerates more typos.

Geometry search parameters

On top of the common parameters:

ParameterTypeDescription
geometriesSearchGeometryInput[]Required. The areas to search: Circle, Polygon, MultiPolygon or a polygon FeatureCollection.

Along-route search parameters

On top of the common parameters:

ParameterTypeDescription
routeRoute | LineString | Position[]Required. A Route from calculateRoute, its LineString geometry, or [longitude, latitude] positions.
maxDetourTimeSecondsnumberRequired. The detour budget, in seconds, as a positive integer up to 3600.
sortBy'detourTime' | 'detourOffset'Order by detour time (default) or by position along the route. A detour time is negative when the route through the place is faster than the original one, so those places come first.

POI categories

filters.poiCategories takes POICategory codes, such as ITALIAN_RESTAURANT, FUEL_STATION, HOTEL, PHARMACY, PARKING_GARAGE or CHARGING_LOCATION. The POI categories service lists them and finds them by name or synonym.

import { discoverPlaces, getPOICategoryCodes } from '@tomtom-org/maps-sdk/services';
const geoBias = { position: [4.9041, 52.3676], radiusMeters: 1000 };
const italian = await discoverPlaces({ geoBias, filters: { poiCategories: ['ITALIAN_RESTAURANT'] } });
// Codes looked up by keyword first
const restaurantCodes = await getPOICategoryCodes({ filters: ['restaurant'] });
const restaurants = await discoverPlaces({ geoBias, filters: { poiCategories: restaurantCodes } });
// Or named in words, in one call
const sushi = await discoverPlaces({ geoBias, filters: { poiCategoryQuery: 'sushi' } });
// EV charging, by connector and power
const fastChargers = await discoverPlaces({
geoBias: { position: [4.9041, 52.3676], radiusMeters: 5000 },
filters: { poiCategories: ['CHARGING_LOCATION'], connectors: ['IEC62196Type2CCS', 'Tesla'], minPowerKW: 50 },
});

A parent code also returns its sub-categories, the codes in its childCategoryCodes. For example, 'SCHOOL' also returns CHILDCARE and DRIVING_SCHOOL places. To leave them out, list only the codes you want, such as 'SCHOOL_GENERIC' for a plain school and 'PRIMARY_SCHOOL'. A place filed under the parent alone comes back only with the parent code. To keep those too, or when your list would pass the 10 codes a search takes, search by the parent and keep the results whose poi.categories holds the parent code or one you want.

Naming the category in words: poiCategoryQuery

filters.poiCategoryQuery takes categories as a person says them, such as 'sushi' or ['gym', 'parking'], and searches the POI categories they name. Every search variant takes it, whether near a geoBias, inside geometries or along a route.

  • The words go through resolvePOICategories in the call’s language, and the codes it returns become the search’s poiCategories. For a language the categories lack, such as hi-IN or ja-JP, the words match the English names.
  • The category list costs one request per language and session, and is then cached.
  • An unmatched word is left out. If no word matches, the call rejects with an SDKError and sends no search. To handle unmatched words yourself, call resolvePOICategories first.
  • When the user names a kind of place, prefer it to query: 'sushi', because query also matches names and addresses.

For a category the user picks while typing, search suggestions return it as a category suggestion whose refine() filters on its code, with no lookup.

In the demo below, a word in the chosen language searches its category inside the Paris outline:

Working with search results

Each feature is a Place with SearchPlaceProps. Places describes every property, including the search-specific ones.

const results = await discoverPlaces({ query: 'coffee', geoBias: { position: [4.9041, 52.3676] } });
results.features.forEach((place) => {
console.log(place.properties.poi?.name); // POI results only
console.log(place.properties.address.freeformAddress);
console.log(place.geometry.coordinates); // [longitude, latitude]
console.log(place.properties.poi?.categories); // POICategory[], e.g. ['CAFE_PUB']
console.log(place.properties.poi?.localizedCategories); // string[] in English, whatever the call's language
console.log(place.properties.distance); // metres from geoBias.position, when given
});

discoverOnePlace() returns the first fuzzy-search result, and throws an SDKError with the code NOT_FOUND when there is none:

import { discoverOnePlace } from '@tomtom-org/maps-sdk/services';
const place = await discoverOnePlace('Eiffel Tower');
// The params form takes the fuzzy search parameters except `limit`
const nearby = await discoverOnePlace({ query: 'coffee', geoBias: { position: [2.2945, 48.8584] } });

Pagination

A fuzzy search response carries nextCursor in its properties while a next page exists. Pass it back as cursor, with the same parameters:

let response = await discoverPlaces({ query: 'cafe', limit: 20 });
const all = [...response.features];
while (response.properties?.nextCursor) {
response = await discoverPlaces({ query: 'cafe', limit: 20, cursor: response.properties?.nextCursor });
all.push(...response.features);
}

The loop spends a request per page up front. To fetch one page per Load more click instead, keep the request and the last nextCursor between clicks, as the search playground does:

import { discoverPlaces } from '@tomtom-org/maps-sdk/services';
import type { FuzzySearchParams } from '@tomtom-org/maps-sdk/services';
const request: FuzzySearchParams = { query: 'cafe', geoBias: { position: [4.9041, 52.3676] }, limit: 20 };
let nextCursor: string | undefined;
const loadPage = async (cursor?: string) => {
loadMoreButton.disabled = true; // a second click before the page lands would fetch it twice
const page = await discoverPlaces({ ...request, cursor });
appendToResultList(page.features);
nextCursor = page.properties?.nextCursor;
loadMoreButton.disabled = !nextCursor;
};
await loadPage();
loadMoreButton.onclick = () => loadPage(nextCursor);
  • The cursor is opaque. Pass it back unchanged. Its contents aren’t part of the contract. A value the SDK can’t read as a cursor throws an SDKError.
  • A cursor continues one search, but the SDK doesn’t check which one. If you pass it with a new query or different filters, that search skips its first pages. Leave cursor out to start a new search.
  • nextCursor, not totalResults, tells whether a next page exists. It is absent once the next page would start at totalResults, after an empty page, and once the next page would start past result 1900, the furthest the backend serves. Fuzzy search’s totalResults is at most 100, and lower at a larger limit, so its pagination ends there.
  • Only fuzzy search and geocoding paginate. Geometry and along-route search return one page, with no nextCursor. geocode pages the same way.

Fields that may be absent

The type is the contract. Where a property is optional, handle its absence instead of assuming a value.

FieldWhat it carries
places.properties.totalResultsThe number of results the search API reports. Fuzzy and geometry search report at most 100, and fuzzy search’s number can drop as limit grows. Along-route search reports the page’s own length. Only geocoding’s is a count of all matches
places.properties.fuzzyLevelHow loosely the query was matched
places.properties.geoBiasThe position results were biased towards
places.properties.queryIntent (fuzzy search)Detection of coordinates, what3words codes, “near X” phrasings and home or work bookmarks
place.properties.scoreA relevance score, for thresholding and merging results
places.bboxA bounding box, on results that cover an area

Geocoding counts matches beyond the page it returns. To show that count, guard totalResults first:

const results = await geocode({ query: 'Main Street', limit: 10 });
if (results.properties?.totalResults !== undefined) {
const { numResults, totalResults } = results.properties;
console.log(`Showing ${numResults} of ${totalResults}`);
}

A missing fuzzyLevel means “not reported”, not “matched exactly”, so don’t read it as 0.

Examples

Find bus stops in an area (Node.js)

A category search confined to a rectangle, as in the Node.js POI search example:

import { discoverPlaces } from '@tomtom-org/maps-sdk/services';
const results = await discoverPlaces({
filters: { poiCategories: ['BUS_STOP'] },
geoBias: { boundingBox: [4.72, 52.27, 5.07, 52.43] }, // [west, south, east, north]
limit: 100,
});
results.features.forEach((stop) => {
const [lng, lat] = stop.geometry.coordinates;
console.log(`${stop.properties.poi?.name} — ${lng}, ${lat}`);
});

Integration with map visualization

The Places module shows a search response without mapping fields first:

import { TomTomMap, PlacesModule } from '@tomtom-org/maps-sdk/map';
import { discoverPlaces } from '@tomtom-org/maps-sdk/services';
const map = new TomTomMap({ mapLibre: { container: 'map' } });
const placesModule = await PlacesModule.create(map);
await placesModule.show(await discoverPlaces({ query: 'museums', geoBias: { position: [4.9041, 52.3676] } }));

Searching again as the map moves

A search re-run on every map move can answer out of order. A slow answer for the previous viewport then paints over the current one. To prevent this, run it through createLatestRequest, as the places customization playground does:

import { createLatestRequest, discoverPlaces } from '@tomtom-org/maps-sdk/services';
const latestSearch = createLatestRequest();
map.mapLibreMap.on('moveend', async () => {
const search = await latestSearch.run((signal) =>
discoverPlaces({
filters: { poiCategories: ['CAFE_PUB'] },
geoBias: { boundingBox: map.getBBox() },
signal,
})
);
if (search.current) await placesModule.show(search.value);
});

To keep POI categories on the map as it moves without writing this yourself, use the viewport places plugin.

Map integration

  • Places module - Search results as interactive places on the map
  • POIs - The map’s own POI icons, filtered by the same categories