Add markers and popups to a MapLibre map
MapLibre GL JS has two ways to put points on a map:
- Markers are HTML elements placed over the map. They're the quickest way to show a few points, and people can drag them.
- Layers draw points from a GeoJSON source as part of the map. They stay fast with thousands of points, and take their size, color, and labels from the data.
This guide shows coffee shops near Pioneer Courthouse Square in Portland both ways, with popups that open on click. The places come from Mapricot's nearby search, and the map uses Mapricot's free tiles. Everything goes in one HTML file, with no build step. It was tested with MapLibre GL JS 6.15 in Chromium.
Before you start
You need a browser key with the Places product. Getting started shows how to make one. Add http://localhost:* to its allowed origins, so it works from your computer while you build. The map tiles need no key.
Make the page
Make a folder for the map, and in it a file called index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Coffee near Pioneer Courthouse Square</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/dist/maplibre-gl.css">
</head>
<body style="margin: 0">
<div id="map" style="height: 100vh"></div>
<script type="module">
import * as maplibregl from "https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/+esm";
const key = "mapricot_pk_…";
async function mapricot(path) {
const response = await fetch(`https://api.mapricot.com${path}`, {
headers: { Authorization: `Bearer ${key}` },
});
if (!response.ok) throw new Error(`Mapricot returned ${response.status}`);
return response.json();
}
const map = new maplibregl.Map({
container: "map",
style: "https://tiles.mapricot.com/styles/light.json",
center: [-122.6793, 45.5189],
zoom: 16,
});
</script>
</body>
</html>
Put your key in place of mapricot_pk_…. Here's what each part does:
- The
linkloads MapLibre's stylesheet, which styles the map's controls, markers, and popups. - The
mapelement is where the map is drawn. It needs a height; this one fills the window. type="module"makes the script a JavaScript module, so it canimportMapLibre and useawaitoutside a function.- The import loads MapLibre from jsDelivr, which serves any npm package.
+esmasks for it as a module, and the version in the URL keeps the page from changing when MapLibre releases a new one. - The key sits in the page, where anyone can read it. That's what browser keys are for: they only work from the origins you allowed.
mapricotsends a request to the API with the key and returns the JSON response, or throws an error if the API refuses the request.new maplibregl.Mapdraws the map in themapelement, with Mapricot's light style, centered on the square. Map tiles lists the other styles.
MapLibre takes coordinates as [longitude, latitude], the reverse of how they're usually written. Mapricot's results have separate lat and lon fields, so keep an eye on the order whenever you pass one to the other.
Open the page
Serve the folder with a web server, rather than opening the file directly. Opened from a file, the map stays blank with Worker failed to load, since browsers don't let a page from a file start MapLibre's web worker. Such a page also has no origin, so the API would refuse the key with 403 and "Key is not allowed from this origin." Any web server works. In the folder, run one of these:
npx serve
python3 -m http.server
npx serve needs Node.js, and serves the page at http://localhost:3000. Python's serves it at http://localhost:8000. Open the address to see a map of downtown Portland, and reload it after each change below.
Add markers
Ask for the 10 coffee shops nearest the square, and add a marker for each, with a popup that opens when the marker is clicked. Add this at the end of the script:
const { results } = await mapricot("/v1/nearby?lat=45.5189&lon=-122.6793&category=food:coffee_shop&limit=10");
for (const place of results) {
new maplibregl.Marker({ color: "#fd7e14" })
.setLngLat([place.lon, place.lat])
.setPopup(new maplibregl.Popup({ offset: 25 }).setText(place.name))
.addTo(map);
}
offset lifts the popup above the pin. The popup uses setText rather than setHTML because place names come from map data that anyone can edit: as text, a name can't add HTML to your page. To show more than text, build the elements yourself and pass them to setDOMContent.
Markers can look like anything: pass your own element as new maplibregl.Marker({ element }). Add draggable: true to let people move one, and listen for its dragend event.
Each marker is an element on the page that moves on every frame as the map pans, so past a hundred or so the map slows down. For more points, use a layer instead. The rest of this guide does.
Show many points as a layer
A layer draws a GeoJSON source. The places become a collection of point features, with whatever properties the layer and popups need. This request finds the 20 coffee shops nearest the square. Click the map to search around another point; the request and response update with each click.
curl "https://api.mapricot.com/v1/nearby?lat=45.5189&lon=-122.6793&category=food:coffee_shop&limit=20" \
-H "Authorization: Bearer $MAPRICOT_KEY"{ "attribution": [ { "source": "overture", "text": "Overture Maps Foundation", "url": "https://docs.overturemaps.org/attribution/" }, { "source": "openstreetmap", "text": "© OpenStreetMap contributors", "url": "https://www.openstreetmap.org/copyright" } ], "results": [ { "id": "mp_b3ZlcnR1cmU6dmVudWU6ZDhkZDIzNWMtY2RlZC00MGIyLTk5MzEtNDEyOGJhZTljY2Rj", "type": "venue", "name": "Starbucks", "label": "Starbucks, Portland, OR, USA", "lat": 45.5191, "lon": -122.679501, "address": { "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food:coffee_shop", "retail", "food" ], "distanceMeters": 27, "source": "overture" }, { "id": "mp_b3BlbnN0cmVldG1hcDp2ZW51ZTpub2RlLzEwOTA0Mzc2NzM1", "type": "venue", "name": "Nordstrom Espresso Bar", "label": "Nordstrom Espresso Bar, Portland, OR, USA", "lat": 45.519215, "lon": -122.679894, "address": { "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food", "retail", "food:coffee_shop" ], "distanceMeters": 58, "source": "openstreetmap" }, { "id": "mp_b3ZlcnR1cmU6dmVudWU6MjdiYzdjYTEtYWY3OS00ZDRiLTg0ODEtYWRlMGZjNDhhNTcx", "type": "venue", "name": "Water Avenue Coffee Company", "label": "Water Avenue Coffee Company, Portland, OR, USA", "lat": 45.51826, "lon": -122.679392, "address": { "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food:coffee_shop", "retail", "food" ], "distanceMeters": 72, "source": "overture" }, { "id": "mp_b3BlbnN0cmVldG1hcDp2ZW51ZTpub2RlLzU4OTcyMjgyMzY", "type": "venue", "name": "Less & More Coffee", "label": "Less & More Coffee, Portland, OR, USA", "lat": 45.518205, "lon": -122.679313, "address": { "houseNumber": "811", "street": "SW 6th Avenue", "postalCode": "97204", "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food", "retail", "food:coffee_shop" ], "distanceMeters": 77, "source": "openstreetmap" }, { "id": "mp_b3BlbnN0cmVldG1hcDp2ZW51ZTpub2RlLzY0NzQyMzE5NTQ", "type": "venue", "name": "Coco Donuts", "label": "Coco Donuts, Portland, OR, USA", "lat": 45.518114, "lon": -122.678909, "address": { "houseNumber": "814", "street": "SW 6th Avenue", "postalCode": "97204", "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food", "retail", "food:coffee_shop" ], "distanceMeters": 93, "source": "openstreetmap" }, { "id": "mp_b3BlbnN0cmVldG1hcDp2ZW51ZTpub2RlLzYzOTAxMTI3Nzg", "type": "venue", "name": "Spella Caffè", "label": "Spella Caffè, Portland, OR, USA", "lat": 45.519737, "lon": -122.678644, "address": { "houseNumber": "608", "street": "SW Alder Street", "postalCode": "97205", "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food", "retail", "food:coffee_shop" ], "distanceMeters": 106, "source": "openstreetmap" }, { "id": "mp_b3BlbnN0cmVldG1hcDp2ZW51ZTpub2RlLzYzOTAwOTQ3NTU", "type": "venue", "name": "Public Domain", "label": "Public Domain, Portland, OR, USA", "lat": 45.519965, "lon": -122.679528, "address": { "houseNumber": "603", "street": "SW Broadway", "postalCode": "97205", "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food", "food:coffee_shop" ], "distanceMeters": 120, "source": "openstreetmap" }, { "id": "mp_b3ZlcnR1cmU6dmVudWU6NTM2ZGUxYzItMjVlNi00OTA0LWJlM2MtOTU3YzJhZjM2OTFl", "type": "venue", "name": "The Park- Cafe, Coffee & Wine Bar", "label": "The Park- Cafe, Coffee & Wine Bar, Portland, OR, USA", "lat": 45.520105, "lon": -122.680524, "address": { "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food:coffee_shop", "retail", "food" ], "distanceMeters": 165, "source": "overture" }, { "id": "mp_b3ZlcnR1cmU6dmVudWU6ZjY4NzVlMzctYzg1Yi00ZjIyLTgwNjUtZjcwZDg4YmY2YTAy", "type": "venue", "name": "Java Man Coffee", "label": "Java Man Coffee, Portland, OR, USA", "lat": 45.520496, "lon": -122.678817, "address": { "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food:coffee_shop", "retail", "food" ], "distanceMeters": 182, "source": "overture" }, { "id": "mp_b3ZlcnR1cmU6dmVudWU6YzBhODViMzMtZTg2My00OGZmLWExMTEtMTI1Mjc1YjIzY2Q5", "type": "venue", "name": "Roseline Coffee", "label": "Roseline Coffee, Portland, OR, USA", "lat": 45.520421, "lon": -122.680344, "address": { "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food:coffee_shop", "retail", "food" ], "distanceMeters": 188, "source": "overture" }, { "id": "mp_b3ZlcnR1cmU6dmVudWU6Y2M2YTE3YjktMjg3Ny00ZDNlLWFkZGEtMWM4NWI4ZGM1OTFm", "type": "venue", "name": "Portland Roasting Coffee", "label": "Portland Roasting Coffee, Portland, OR, USA", "lat": 45.517532, "lon": -122.67785, "address": { "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food:coffee_shop", "retail", "food" ], "distanceMeters": 190, "source": "overture" }, { "id": "mp_b3ZlcnR1cmU6dmVudWU6NzcxMzAyZjMtNzYzNS00ZWQ0LWExNzQtZmUyYzIxZjg1ZTNk", "type": "venue", "name": "Peet's Coffee", "label": "Peet's Coffee, Portland, OR, USA", "lat": 45.520568, "lon": -122.678795, "address": { "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food:coffee_shop", "retail", "food" ], "distanceMeters": 190, "source": "overture" }, { "id": "mp_b3ZlcnR1cmU6dmVudWU6ZjNkZTQ5ZmEtODE2YS00OWFmLTg4ODMtMWYyYjA2ODAzMTdm", "type": "venue", "name": "Tully's Coffee", "label": "Tully's Coffee, Portland, OR, USA", "lat": 45.520483, "lon": -122.68104, "address": { "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food:coffee_shop", "retail", "food" ], "distanceMeters": 222, "source": "overture" }, { "id": "mp_b3BlbnN0cmVldG1hcDp2ZW51ZTpub2RlLzEyOTQxNDI5OTAx", "type": "venue", "name": "40 LBS Coffee Bar", "label": "40 LBS Coffee Bar, Portland, OR, USA", "lat": 45.5205, "lon": -122.677563, "address": { "houseNumber": "400", "street": "SW 6th Avenue", "postalCode": "97204", "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food", "retail", "food:coffee_shop" ], "distanceMeters": 224, "source": "openstreetmap" }, { "id": "mp_b3ZlcnR1cmU6dmVudWU6ZWM4MTFiYmItZGU0ZS00NjkyLTgyMWMtYzRhYjEzZGI4NDBh", "type": "venue", "name": "Forte Portland", "label": "Forte Portland, Portland, OR, USA", "lat": 45.516994, "lon": -122.677932, "address": { "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food:coffee_shop", "retail", "food" ], "distanceMeters": 237, "source": "overture" }, { "id": "mp_b3BlbnN0cmVldG1hcDp2ZW51ZTpub2RlLzY0MDU1MjE4NTg", "type": "venue", "name": "Case Study Coffee Roasters", "label": "Case Study Coffee Roasters, Portland, OR, USA", "lat": 45.519173, "lon": -122.68233, "address": { "houseNumber": "802", "street": "SW 10th Avenue", "postalCode": "97205", "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food", "retail", "food:coffee_shop" ], "distanceMeters": 238, "source": "openstreetmap" }, { "id": "mp_b3BlbnN0cmVldG1hcDp2ZW51ZTpub2RlLzM2NTczMDUzOTk", "type": "venue", "name": "Anna's Chocolate Cafe", "label": "Anna's Chocolate Cafe, Portland, OR, USA", "lat": 45.516932, "lon": -122.67795, "address": { "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food", "retail", "food:coffee_shop" ], "distanceMeters": 243, "source": "openstreetmap" }, { "id": "mp_b3ZlcnR1cmU6dmVudWU6OWEzMTcxMzgtODQ4NC00Zjc3LWI4YjUtMzI5YTA5NWIyZjg4", "type": "venue", "name": "Bridgetown Caffe", "label": "Bridgetown Caffe, Portland, OR, USA", "lat": 45.516588, "lon": -122.678882, "address": { "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food:coffee_shop", "retail", "food" ], "distanceMeters": 259, "source": "overture" }, { "id": "mp_b3BlbnN0cmVldG1hcDp2ZW51ZTp3YXkvMzk3ODA2MzEy", "type": "venue", "name": "Less & More Coffee", "label": "Less & More Coffee, Portland, OR, USA", "lat": 45.516584, "lon": -122.678882, "address": { "houseNumber": "1003", "street": "SW 5th Avenue", "postalCode": "97204", "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food", "retail", "food:coffee_shop" ], "distanceMeters": 260, "source": "openstreetmap" }, { "id": "mp_b3ZlcnR1cmU6dmVudWU6YzZlNmM3MTAtYTkxOS00ZjQzLTgyZDctOGU1YzRkNmEyYjhk", "type": "venue", "name": "Tous Les Jours", "label": "Tous Les Jours, Portland, OR, USA", "lat": 45.521194, "lon": -122.680783, "address": { "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "categories": [ "food:coffee_shop", "retail", "food" ], "distanceMeters": 280, "source": "overture" } ]}Take out the markers code, and in its place add a source and two layers: orange circles for the places, and their names below them:
function toGeoJSON(places) {
return {
type: "FeatureCollection",
features: places.map((place) => ({
type: "Feature",
properties: { name: place.name },
geometry: { type: "Point", coordinates: [place.lon, place.lat] },
})),
};
}
await map.once("load");
const { results } = await mapricot("/v1/nearby?lat=45.5189&lon=-122.6793&category=food:coffee_shop&limit=20");
map.addSource("places", {
type: "geojson",
data: toGeoJSON(results),
attribution: '<a href="https://docs.overturemaps.org/attribution/">Overture Maps Foundation</a>',
});
map.addLayer({
id: "places",
type: "circle",
source: "places",
paint: {
"circle-radius": 6,
"circle-color": "#fd7e14",
"circle-stroke-color": "#ffffff",
"circle-stroke-width": 2,
},
});
map.addLayer({
id: "place-names",
type: "symbol",
source: "places",
layout: {
"text-field": ["get", "name"],
"text-font": ["Noto Sans Medium"],
"text-size": 13,
"text-offset": [0, 1],
"text-anchor": "top",
},
paint: { "text-color": "#212529", "text-halo-color": "#ffffff", "text-halo-width": 1.5 },
});
- Sources and layers can only be added once the style has loaded, which
map.once("load")waits for. Call it straight after creating the map: if the code awaits something else first, the map can finish loading before anything is listening. ["get", "name"]reads each feature'snameproperty. The same expressions can size or color points by any property.text-fonthas to be a font the style has. Mapricot's styles have Noto Sans Regular, Medium, and Italic.- Where labels would overlap, MapLibre hides some of them, and shows them again as you zoom in.
- The source's
attributionadds a credit to the map's corner, as Credits explains.
Open a popup on click
Layers don't have popups of their own. Listen for clicks on the layer, and open a popup at the feature that was clicked. Add this at the end of the script:
map.on("click", "places", (event) => {
const [place] = event.features;
new maplibregl.Popup({ offset: 10 })
.setLngLat(place.geometry.coordinates)
.setText(place.properties.name)
.addTo(map);
});
map.on("mouseenter", "places", () => (map.getCanvas().style.cursor = "pointer"));
map.on("mouseleave", "places", () => (map.getCanvas().style.cursor = ""));
Passing the layer's ID to map.on limits the event to its features, and event.features lists the ones under the pointer. The cursor change shows people the points can be clicked.
Search this area
To show the coffee shops wherever someone has moved the map, add a button above the map element, in the body:
<button id="search-here" style="position: absolute; z-index: 1; top: 10px; left: 10px">Search this area</button>
The style puts it over the top-left corner of the map. Then, at the end of the script, give the source new places when the button is clicked:
function nearby(center) {
const { lat, lng } = center.wrap();
return mapricot(`/v1/nearby?lat=${lat.toFixed(6)}&lon=${lng.toFixed(6)}&category=food:coffee_shop&limit=20`);
}
document.querySelector("#search-here").addEventListener("click", async () => {
const { results } = await nearby(map.getCenter());
map.getSource("places").setData(toGeoJSON(results));
});
setData replaces the source's points, and both layers redraw. Two details keep the request valid: wrap brings the longitude back between -180 and 180 after the map has been panned around the world, and toFixed(6) rounds the coordinates, which MapLibre gives to about 14 decimal places. Mapricot accepts at most 10, and six is already accurate to about 4 inches.
Fit the map to the points
To frame the first results instead of picking a zoom level, fit the map to their bounds. Add this at the end of the script:
const bounds = new maplibregl.LngLatBounds();
for (const place of results) bounds.extend([place.lon, place.lat]);
map.fitBounds(bounds, { padding: 40, maxZoom: 17 });
padding keeps points off the edges, and maxZoom stops a single result from zooming all the way in.
The whole page
Here's index.html with everything above, ready to copy. Put your key in place of mapricot_pk_…:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Coffee near Pioneer Courthouse Square</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/dist/maplibre-gl.css">
</head>
<body style="margin: 0">
<button id="search-here" style="position: absolute; z-index: 1; top: 10px; left: 10px">Search this area</button>
<div id="map" style="height: 100vh"></div>
<script type="module">
import * as maplibregl from "https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/+esm";
const key = "mapricot_pk_…";
async function mapricot(path) {
const response = await fetch(`https://api.mapricot.com${path}`, {
headers: { Authorization: `Bearer ${key}` },
});
if (!response.ok) throw new Error(`Mapricot returned ${response.status}`);
return response.json();
}
const map = new maplibregl.Map({
container: "map",
style: "https://tiles.mapricot.com/styles/light.json",
center: [-122.6793, 45.5189],
zoom: 16,
});
function toGeoJSON(places) {
return {
type: "FeatureCollection",
features: places.map((place) => ({
type: "Feature",
properties: { name: place.name },
geometry: { type: "Point", coordinates: [place.lon, place.lat] },
})),
};
}
await map.once("load");
const { results } = await mapricot("/v1/nearby?lat=45.5189&lon=-122.6793&category=food:coffee_shop&limit=20");
map.addSource("places", {
type: "geojson",
data: toGeoJSON(results),
attribution: '<a href="https://docs.overturemaps.org/attribution/">Overture Maps Foundation</a>',
});
map.addLayer({
id: "places",
type: "circle",
source: "places",
paint: {
"circle-radius": 6,
"circle-color": "#fd7e14",
"circle-stroke-color": "#ffffff",
"circle-stroke-width": 2,
},
});
map.addLayer({
id: "place-names",
type: "symbol",
source: "places",
layout: {
"text-field": ["get", "name"],
"text-font": ["Noto Sans Medium"],
"text-size": 13,
"text-offset": [0, 1],
"text-anchor": "top",
},
paint: { "text-color": "#212529", "text-halo-color": "#ffffff", "text-halo-width": 1.5 },
});
map.on("click", "places", (event) => {
const [place] = event.features;
new maplibregl.Popup({ offset: 10 })
.setLngLat(place.geometry.coordinates)
.setText(place.properties.name)
.addTo(map);
});
map.on("mouseenter", "places", () => (map.getCanvas().style.cursor = "pointer"));
map.on("mouseleave", "places", () => (map.getCanvas().style.cursor = ""));
function nearby(center) {
const { lat, lng } = center.wrap();
return mapricot(`/v1/nearby?lat=${lat.toFixed(6)}&lon=${lng.toFixed(6)}&category=food:coffee_shop&limit=20`);
}
document.querySelector("#search-here").addEventListener("click", async () => {
const { results } = await nearby(map.getCenter());
map.getSource("places").setData(toGeoJSON(results));
});
const bounds = new maplibregl.LngLatBounds();
for (const place of results) bounds.extend([place.lon, place.lat]);
map.fitBounds(bounds, { padding: 40, maxZoom: 17 });
</script>
</body>
</html>
Credits
The map style credits OpenStreetMap. Nearby results also come from the Overture Maps Foundation, as the response's attribution lists. The source's attribution option adds that credit to the map's corner while the layer is on the map.
Questions
Should I use markers or a layer? Markers for a handful of points, or when people need to drag them. A layer for anything more, or when the points' look depends on their data.
Why is my marker in the wrong place? Almost always because latitude and longitude are swapped. MapLibre takes [longitude, latitude]. Swapped, a place more than 90 degrees east or west, such as anywhere in the Americas, throws Invalid LngLat latitude value: must be between -90 and 90, and anywhere else lands somewhere else entirely, often in the ocean.
Why does the map show but no coffee shops? Open the browser's console. Mapricot returned 403 means the API refused the key: check that the page is served from an origin the key allows, such as http://localhost:*, and not opened as a file, and that the key has the Places product. Mapricot returned 400 means a malformed request, often coordinates with too many decimal places.
How do I use my own icon in a layer? Load the image and add it to the map, then use a symbol layer with icon-image instead of a circle layer:
const image = await map.loadImage("coffee.png");
map.addImage("coffee", image.data);
map.addLayer({ id: "places", type: "symbol", source: "places", layout: { "icon-image": "coffee" } });
How many requests does this use? One for each nearby search. The map tiles are free and don't count. Clustering points shows hundreds of places at once, and drawing a route adds the way to get to one.