Leaflet + PickPoint
Leaflet was created by Volodymyr Agafonkin in 2011 and quickly became the most widely used open-source JavaScript mapping library. Its philosophy is deliberate simplicity: the core is just 42 KB gzipped, the API is minimal and stable, and it works in every browser without a build step. You load it from CDN, write plain JavaScript, and have a working map in minutes.
That simplicity is the point. If you need 3D buildings or GPU-powered rendering, MapLibre is the better fit. If you need a map that ships fast and runs everywhere - on slow phones, corporate proxies, and IE 11 - Leaflet is the right choice. We use OpenStreetMap raster tiles here: free, no API key, and familiar to any user who has used a map on the web.
HTML scaffold
Load Leaflet from CDN. Full-screen map with a floating search box:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<style>
html, body, #map { height: 100%; margin: 0; }
#search-box { position: absolute; top: 12px; right: 12px; z-index: 1000;
background: #fff; padding: 8px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,.15); min-width: 260px; }
#query { width: 100%; box-sizing: border-box; padding: 6px 10px;
border: 1px solid #ccc; border-radius: 4px; font-size: 14px; }
#suggestions { list-style: none; margin: 4px 0 0; padding: 0; }
#suggestions li { padding: 6px 10px; cursor: pointer; border-radius: 4px;
font-size: 13px; }
#suggestions li:hover { background: #f0f7f7; }
</style>
</head>
<body>
<div id="map"></div>
<div id="search-box">
<input id="query" type="search" placeholder="Search destination…" autocomplete="off">
<ul id="suggestions"></ul>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="app.js"></script>
</body>
</html>Initialise the map
// app.js
const API_KEY = 'YOUR_PICKPOINT_KEY';
const map = L.map('map').setView([51.505, -0.09], 13);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://openstreetmap.org">OpenStreetMap</a>',
maxZoom: 19,
}).addTo(map);
let originMarker = null;
let destMarker = null;
let routeLine = null;Get the user's position as the origin
navigator.geolocation.getCurrentPosition(({ coords }) => {
const latlng = [coords.latitude, coords.longitude];
map.setView(latlng, 14);
// Build icon via DOM API to keep source code free of HTML tags
const dot = document.createElement('div');
Object.assign(dot.style, {
width: '14px', height: '14px', borderRadius: '50%',
background: '#008080', border: '2px solid #fff',
boxShadow: '0 1px 4px rgba(0,0,0,.4)',
});
originMarker = L.marker(latlng, {
icon: L.divIcon({ className: '', html: dot.outerHTML, iconSize: [14,14], iconAnchor: [7,7] }),
}).addTo(map);
});Address autocomplete
Call /v2/address/search on each keystroke (debounced 250 ms). Pass the visible map bounds as viewbox to bias results:
const input = document.getElementById('query');
const list = document.getElementById('suggestions');
let debounce;
input.addEventListener('input', () => {
clearTimeout(debounce);
const q = input.value.trim();
if (q.length < 2) { list.innerHTML = ''; return; }
debounce = setTimeout(async () => {
const b = map.getBounds();
// /v2/address/search uses bbox=minLon,minLat,maxLon,maxLat
const bbox = `${b.getWest()},${b.getSouth()},${b.getEast()},${b.getNorth()}`;
const res = await fetch(
`https://api.pickpoint.io/v2/address/search?q=${encodeURIComponent(q)}&limit=6&bbox=${bbox}`,
{ headers: { 'X-Api-Key': API_KEY } }
);
const { features = [] } = await res.json();
// Response is GeoJSON: coordinates = [lon, lat], properties.name = label
list.innerHTML = features.map(f => {
const [lon, lat] = f.geometry.coordinates;
const label = [f.properties.name, f.properties.city].filter(Boolean).join(', ');
return `<li data-lat="${lat}" data-lon="${lon}">${label}</li>`;
}).join('');
}, 250);
});
list.addEventListener('click', (e) => {
const li = e.target.closest('li');
if (!li) return;
const lat = parseFloat(li.dataset.lat);
const lon = parseFloat(li.dataset.lon);
input.value = li.textContent;
list.innerHTML = '';
destMarker?.remove();
destMarker = L.marker([lat, lon]).addTo(map);
if (originMarker) drawRoute(originMarker.getLatLng(), [lat, lon]);
});Draw the route and track live position
trip.legs[0].shape is an array of [lat, lon] pairs - pass it directly to L.polyline(). For real-time position of the vehicle on the map, subscribe to the Device Tracking WebSocket:
async function drawRoute(origin, dest) {
const res = await fetch('https://api.pickpoint.io/v2/route', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Api-Key': API_KEY },
body: JSON.stringify({
locations: [
{ lat: origin.lat, lon: origin.lng },
{ lat: dest[0], lon: dest[1] },
],
costing: 'auto',
}),
});
const data = await res.json();
const coords = data.trip.legs[0].shape; // [[lat, lon], …] - Leaflet-ready
routeLine?.remove();
routeLine = L.polyline(coords, { color: '#008080', weight: 4 }).addTo(map);
map.fitBounds(routeLine.getBounds(), { padding: [40, 40] });
}
// Real-time position updates via PickPoint Device Tracking WebSocket
// npm install socket.io-client
import { io } from 'socket.io-client';
const vehicleMarker = L.marker([0, 0]).addTo(map);
const socket = io('wss://ws.pickpoint.io', {
transports: ['websocket'],
path: '/v2/tracking',
query: { 'x-api-key': API_KEY }, // subscriber auth - keep on server!
});
socket.on('connect', () => {
socket.emit('device:subscribe', { deviceUid: 'DEVICE_UID' });
});
socket.on('location:added', ({ latitude, longitude }) => {
vehicleMarker.setLatLng([latitude, longitude]);
map.panTo([latitude, longitude]);
});Going further
- Turn-by-turn panel - render
trip.legs[0].maneuversas a list. - Vehicle profile - change
costingtobicycle,pedestrian, ortruck. - Reverse geocode on click - on map click, call
/v2/geocode/reverseand show the address in a popup.