This is the documentation of version 0.62.0. Go to the latest version, 1.0.0-rc.5.

Places module

The Places Module allows you to display and interact with places on your TomTom map. You can add search results, custom locations, and handle user interactions.

What this module owns: the place markers and labels it draws, and how they look — the marker type, colour, icon and label configuration below. It never restyles the base map underneath, which belongs to the Styling module. Markers given no colour read the map's accent as their default, and the marker types that borrow the look of the style's own POIs follow the knobs that restyle them (Borrowed looks). See Who owns what.

Initialization

import { TomTomMap, PlacesModule } from '@tomtom-org/maps-sdk/map';

// Initialize map
const map = new TomTomMap({ mapLibre: { container: 'map' } });

// Initialize Places module with default configuration
const placesModule = await PlacesModule.create(map);

Displaying places

Display one or multiple places on the map:

import { discoverPlaces } from '@tomtom-org/maps-sdk/services';

// Show single place
await placesModule.show({
  id: 'custom-place-1',
  type: 'Feature',
  geometry: { type: 'Point', coordinates: [4.9041, 52.3676] },
  properties: {
    type: 'POI',
    address: { freeformAddress: 'Custom Place 1, Amsterdam' },
    poi: { name: 'Custom Place' }
  }
});

// Show multiple places
await placesModule.show([place1, place2, place3]);

const searchResults = await discoverPlaces({
    query: 'restaurants',
    geoBias: { position: [4.9041, 52.3676] }, // Amsterdam
    limit: 10
});

// Show search results
await placesModule.show(searchResults);

// Remove all places from the map:
await placesModule.clear();

What show() takes

show() takes a place, an array of places or a Places collection as the service returns it: search, geocoding, reverse geocoding, place details and EV availability results, or your own GeoJSON points. There are no fields to map first. Three places services return something else:

Result Not showable because Show instead
getSearchSuggestions Suggestions to choose from, without geometry A place suggestion's resolve(), or a brand or category suggestion's refine()
geometryData Polygons The Geometries module
evChargingStationsAvailability Availability alone, no place getPlacesWithEVAvailability, over the stations a search found

preparePlacesForDisplay is what show() does to each place before drawing it. Call it to see the properties your layer overrides, extra properties and event handlers read:

import { preparePlacesForDisplay } from '@tomtom-org/maps-sdk/map';

const [first] = preparePlacesForDisplay(searchResults, { markerType: 'base-map' }).features;
console.log(first.properties.title, first.properties.iconID, first.properties.category);

Your own data

Your own GeoJSON points go to show() as they are, and draw like the places a search returns. Each keeps its own properties, which label.title, layer overrides and click handlers read back. Name their shape on create to have the label.title and icon.mapping callbacks typed for it:

type Store = CommonPlaceProps & { storeName: string };
const stores: Places<Store> = await (await fetch('https://your-api.com/stores.json')).json();

const placesModule = await PlacesModule.create<Store>(map, {
    markerType: 'base-map', // drawn like the map style's own POIs
    icon: { mapping: { to: 'poiCategory', fn: () => 'COFFEE_SHOP' } },
    label: { title: (place) => place.properties.storeName },
});

await placesModule.show(stores);

Showing and hiding

setVisible hides and shows everything the module draws — places, connections and entry points — while keeping its data. visible: false at creation starts it hidden.

const placesModule = await PlacesModule.create(map, { visible: false });
await placesModule.show(searchResults); // loaded, not drawn

placesModule.setVisible(true);
placesModule.isVisible(); // true

Custom styling

Marker type

The marker type sets the kind of marker places are drawn with. Choose one at initialization or swap at runtime:

// Initialize with a specific marker type
const placesModule = await PlacesModule.create(map, { markerType: 'base-map' });

// Or swap at runtime
placesModule.updateConfig({ markerType: 'circle-icon' });

// Micro-only rendering: combine `base-map` with a hidden `main` layer
const placesModule = await PlacesModule.create(map, {
    markerType: 'base-map',
    layers: { main: { layout: { visibility: 'none' } } },
});

base-map, circle-icon and pin-clustered borrow the look of the style's POIs, so the styling knobs that restyle those POIs restyle them too; which knobs per marker type is in Borrowed looks.

Clusters

Under the pin-clustered marker type, cluster sets how places cluster and how the count badge looks. badge styles the badge of single-category and mixed-category clusters together, and source takes MapLibre's cluster options:

const placesModule = await PlacesModule.create(map, {
    markerType: 'pin-clustered',
    // The per-place value the cluster aggregates
    extraFeatureProps: {
        connectorCount: (place) =>
            place.properties.chargingPark?.connectors.reduce((total, { count }) => total + count, 0) ?? 0,
    },
    cluster: {
        badge: { color: '#3f9cd9', radius: { single: 13, mixed: 18 } },
        source: {
            clusterRadius: 100, // default 60
            clusterMaxZoom: 14, // default 17
            // Total charging points across each cluster
            clusterProperties: { connectors: ['+', ['get', 'connectorCount']] },
        },
    },
    // An aggregated value lands on the cluster feature's properties
    layers: { clusterCount: { layout: { 'text-field': ['get', 'connectors'] } } },
});

Colour

color is the colour of the module's own drawing: the default pin fill, the label of a hovered or clicked place, connection lines and entry points. Connection labels take a darker shade of it on a light map and a lighter one on a dark map; entry points take the darker one on a light map.

const placesModule = await PlacesModule.create(map, { color: '#2A9D8F' });

Connections

showConnections draws a dashed line between pairs of shown places, labelled by the label function of connections. connections.color, connections.textColor and connections.haloColor set their line, label and halo colours over the module's color; unset, the label takes a shade of the line's colour that reads on the map's light or dark theme.

const placesModule = await PlacesModule.create(map, {
    connections: { color: '#1F9D6B', label: (connection) => `${connection.minutes} min` },
});
await placesModule.show(places);
await placesModule.showConnections([{ from: 'station', to: 'cafe', minutes: 4 }]);

Label styling

Customize the labels next to place markers with the label configuration:

// Make POI names red and other places blue
const customLabelConfig: PlaceLabelConfig = {
    color: ['match', ['get', 'type'], 'POI', '#D32F2F', '#1976D2']
};

// Initialize with custom label styling
const placesModule = await PlacesModule.create(map, {
    label: customLabelConfig,
});

// Or change after initialization
placesModule.updateConfig({ label: customLabelConfig });

PlaceLabelConfig takes the label settings shared across modules (size, color, haloColor, haloWidth, opacity and font) plus the place's own title and offset. The label given to updateConfig replaces the whole label (changing one field):

placesModule.updateConfig({
    label: {
        title: (place) => place.properties.poi?.name,
        opacity: 0.8,
        haloWidth: 2,
    },
});

Set labels through label rather than with MapLibre's setPaintProperty on the module's layers. A style change and a marker type change rebuild those layers from the module's configuration, so the label configuration carries over while a paint property written on a layer is lost. A title computed from each place has no MapLibre equivalent at all.

A layers.* override, such as layers.main.layout['text-size'], wins over label for the properties it names.

Icon styling

Customize place icons and markers. Custom icons work under every marker type: when a category is mapped, features in that category use the provided sprite; otherwise the marker type's default icon (the base-map style's category/group expression or the default pin) still renders.

// Per-category custom images — layered on top of any marker type
const customIconConfig: PlaceIconConfig = {
  categoryIcons: [
    { id: 'RESTAURANT', image: '/icons/food.png', pixelRatio: 2 },
    { id: 'HOTEL_OR_MOTEL', image: '/icons/hotel.png', pixelRatio: 2 }
  ]
};

// Initialize with custom icon styling
const placesModule = await PlacesModule.create(map, {
  markerType: 'pin',
  icon: customIconConfig
});

// Or change after initialization
placesModule.updateConfig({ icon: customIconConfig });

Use offsetX/offsetY (pixels) on a categoryIcons entry to shift a custom icon from its coordinate — useful when the icon's visual tip isn't at its own geometric bottom or center:

const customIconConfig: PlaceIconConfig = {
  categoryIcons: [
    { id: 'RESTAURANT', image: '/icons/food.png', pixelRatio: 2, offsetX: 0, offsetY: -10 }
  ]
};

offsetX/offsetY only apply to categoryIcons entries that also provide an image — an entry that references an existing sprite icon by id alone ignores them. Setting them on PlaceIconConfig.default.image has no effect. RoutingModule charging stop icons support the same two fields; see the routes guide.

Extra properties

Add custom properties to places for advanced styling or filtering:

placesModule.updateConfig({
    extraFeatureProps: {
        category: (place) => place.properties.poi?.categories?.[0],
        brand: (place) => place.properties.poi?.brands?.[0] ?? '',
        isOpen: true,
    },
});

A callback receives a Place and must return a JSON value. When the places you show carry a narrower shape — EV charging stations with availability, say — name it on the config, or as the type parameter of applyConfig / updateConfig, and the callbacks are typed for it, along with label.title and icon.mapping:

const config: PlacesModuleConfig<EVChargingStationWithAvailabilityPlaceProps> = {
    extraFeatureProps: {
        availabilityRatio: (place) => getChargingPointAvailability(place)?.ratio ?? 0,
    },
};

Layer positioning

By default the markers sit above the map's layers, and the connection lines below its labels. beforeLayerConfig moves them: one target for everything the module draws — 'top' or a key of mapStyleLayerIDs — or a record keyed by part, plus all for the parts it leaves out. The parts are named after the event scopes:

// The markers under the map's labels; the connections stay where they are
placesModule.updateConfig({ beforeLayerConfig: { places: 'lowestLabel' } });

Each part keeps its own layers in order wherever it goes, and applyConfig or updateConfig restacks them at runtime; resetConfig() puts them back. Given the same target, the connections stay under the markers. A target the style lacks — the satellite style has no lowestRoadLine — puts the part on top.

Entry points

Search, geocoding and reverse geocoding fill a place's entrances in properties.entryPoints. Set entryPoints on the module config to draw them: a dot per entrance, larger for the main one, with a dotted line back to the place.

const placesModule = await PlacesModule.create(map, {
    entryPoints: {
        showFor: 'clicked', // 'all' | 'selected' (hovered or clicked, the default) | 'clicked'
        minZoom: 16,        // default 15
        layers: { line: { layout: { visibility: 'none' } } }, // MapLibre overrides for `point` and `line`
    },
});

// Only places with an event handler reach the hover and click states
placesModule.events.on('click', (feature) => console.log(feature.properties.title));

// `entryPoints: undefined` stops drawing them
placesModule.updateConfig({ entryPoints: { showFor: 'all' } });

A reverse geocoding result's geometry is the queried point; move it to properties.originalPosition, the matched address, before showing it:

const address = await reverseGeocode({ position: clickedPosition });
await placesModule.show({
    ...address,
    geometry: { ...address.geometry, coordinates: address.properties.originalPosition },
});

Every setting as data

placesKnobCatalogue lists every plain-valued display setting of the module with its kind, its range or options and, where one value holds everywhere, its default. Each id is the setting's path in the config, so a UI can build its controls from it, with no map loaded:

import { placesKnobCatalogue } from '@tomtom-org/maps-sdk/map';

const badgeKnobs = placesKnobCatalogue.filter(({ id }) => id.startsWith('cluster.badge.'));
// [{ id: 'cluster.badge.color', kind: 'color', description: '…', default: '#000000' }, …,
//  { id: 'cluster.badge.radius.single', kind: 'number', description: '…', range: { min: 4, max: 32, step: 1, bounds: 'hard-min' }, default: 11 }, …]
Knob Kind Default
markerType enum 'pin'
color color the map's accent
visible toggle true
icon.default.style.fillColor color color
icon.default.style.outlineColor color
icon.default.style.outlineOpacity number
label.size number 14 to 16 pixels by zoom
label.color color by the map's light/dark theme
label.haloColor color by the map's light/dark theme
label.haloWidth number 1 to 1.5 pixels by zoom
label.opacity number
label.offset number from the icon's size
label.font enums Noto-Bold, or the style's under base-map
cluster.badge.color color '#000000'
cluster.badge.strokeColor color '#FFFFFF'
cluster.badge.strokeWidth number 1.5
cluster.badge.textColor color '#FFFFFF'
cluster.badge.textSize number 12
cluster.badge.radius.single number 11
cluster.badge.radius.mixed number 15
cluster.badge.offset offset [12, -14]
cluster.source.clusterRadius number 60
cluster.source.clusterMaxZoom number 17
cluster.source.clusterMinPoints number MapLibre's, 2
evAvailability.enabled toggle false
evAvailability.threshold number 0.3
entryPoints.showFor enum 'selected'
entryPoints.minZoom number 15
connections.color color the module's color, else by the map's light/dark theme
connections.textColor color a shade of the line's colour for the map's light/dark theme
connections.haloColor color by the map's light/dark theme

Events

User events

Handle user interactions with places:

// Click events
placesModule.events.on('click', (feature, lngLat) => {
    console.log('Click coordinates:', lngLat);
    console.log('Place title:', feature.properties.title);
    console.log('Place coordinates:', feature.geometry.coordinates);
});

// Hover events
placesModule.events.on('hover', (feature, lngLat) => {
    console.log('Hovering over:', feature.properties.poi?.name);
});

// Remove event listeners
placesModule.events.off('click', handler);

events covers every surface the module draws. Three named scopes narrow it: events.places for the pins alone — typed as Place, which is what you want in TypeScript — events.connections for the lines and labels drawn by showConnections, and events.entryPoints for the entry points:

placesModule.events.places.on('click', (place) => console.log(place.properties.poi?.name));
placesModule.events.connections.on('click', () => console.log('connection line clicked'));
placesModule.events.entryPoints.on('click', (entryPoint) => {
    // A point at the entrance, whether its dot or its line was hit, with its place attached
    console.log(entryPoint.properties.type, entryPoint.properties.place?.properties.title);
    // It is a valid route location
    calculateRoute({ locations: [origin, entryPoint] });
});

Result cards and popups

The module tells you which place was clicked or hovered, and does the hit-testing behind it. It draws no result card, popup or info window for a place: open your own from the event.

import { Popup } from 'maplibre-gl';

const popup = new Popup({ closeButton: false, offset: 35 });

placesModule.events.places.on('click', (place) => {
    popup
        .setLngLat(place.geometry.coordinates)
        .setText(place.properties.poi?.name ?? place.properties.address.freeformAddress)
        .addTo(map.mapLibreMap);
});

// The popup is yours, so close it when the places change
popup.remove();
await placesModule.show(newResults);

Programmatic event states

Put, clean and read the states of places the way you do on every module that draws your data — see event states. They act on the places, whichever marker type draws them, clustered or not; events.connections and events.entryPoints carry their own.

// Highlight the first place as if clicked
placesModule.putEventState({ index: 0, state: 'click' });

const { click: [selectedID] = [], hover = [] } = placesModule.getEventStates();

placesModule.cleanEventStates({ states: ['hover'] });

A pin draws its state, and so do its entry points. A show() of new results drops the states; to keep a selection across a re-show, read it first and put it back:

const [selectedID] = placesModule.getEventStates().click ?? [];
await placesModule.show(morePlaces);
if (selectedID) placesModule.putEventState({ id: selectedID, state: 'click' });

Multiple module instances

You can create multiple independent instances of the Places module, each with its own configuration and data. This is useful for displaying different sets of places with distinct styling or behavior.

// Create separate instances for different purposes
const restaurantPlaces = await PlacesModule.create(map, {
    markerType: 'pin',
    icon: {
        categoryIcons: [{ id: 'RESTAURANT', image: '/icons/restaurant.png', pixelRatio: 2 }]
    }
});

const hotelPlaces = await PlacesModule.create(map, {
    markerType: 'circle-icon',
    icon: {
        categoryIcons: [{ id: 'HOTEL_OR_MOTEL', image: '/icons/hotel.png', pixelRatio: 2 }]
    }
});

// Each instance manages its own places independently
await restaurantPlaces.show(restaurantResults);
await hotelPlaces.show(hotelResults);

Reading shown data

Use getShown() to retrieve the places currently displayed on the map: the places passed to show() as drawn, each with its display properties (title, iconID…) and current eventState, and the connections passed to showConnections():

const shown = placesModule.getShown();
console.log(`Showing ${shown.places.features.length} places`);

Identity across show, clear and style changes

What happens to the shown places, their event states and their connections when you show again, clear or change the style:

Call Places Event states Connections
show(results) Replaced, never merged Dropped, even for a place shown again under the same id Dropped
show(placesModule.getShown().places) Shown again Kept: they travel on each feature's properties.eventState Dropped
clear() Removed Removed Dropped
applyConfig, updateConfig, setKnob Kept Kept Kept, ids resolved again
setStyle Kept Kept Kept, ids resolved again
setStyle(style, { resetState: true }) Removed Removed Removed

So in a search, select, search again loop, put the selection back by id after the new show():

// `selectedID` is the id your app kept when the place was clicked
await placesModule.show(await discoverPlaces({ query: 'coffee', geoBias: { position } }));
placesModule.putEventState({ id: selectedID, state: 'click' }); // does nothing if the id is gone

API reference

For complete documentation of all Places Module properties, methods, and types, see the PlacesModule API Reference.

Related guides and examples

Services integration

Related examples

Related map modules