Last-mile delivery is the most expensive part of any supply chain. Industry benchmarks consistently put last-mile costs at 53% of total shipping cost. Within that, the biggest controllable variable is route efficiency. A driver completing 15 stops in the wrong order may drive 40% more kilometres than necessary, burning fuel, wearing out the vehicle, and missing delivery time windows that trigger redelivery fees.
Route optimisation is not a hard problem to solve in code anymore. What used to require expensive specialist logistics software is now an API call. This tutorial builds a complete multi-stop route optimiser using the MapAtlas Routing API: a Python script that sends a list of delivery stops, gets back an optimised sequence with total distance and time, applies time window constraints, and handles EU Low Emission Zone restrictions for urban deliveries. A JavaScript snippet then draws the result on a map.
The Python implementation is under 55 lines. The JavaScript map display is an additional 30.
The Last-Mile Cost Problem
To understand what optimisation actually saves, run the numbers for a realistic delivery scenario:
- Fleet: 10 vans
- Stops per van per day: 18
- Current average distance: 210 km/van/day
- Fuel cost: €0.38/km (diesel, EU average)
- Driver cost: €22/hour
- Average current route time: 7.5 hours/day
Current daily cost per van: (210 × €0.38) + (7.5 × €22) = €79.80 + €165 = €244.80/van/day
A 30% distance reduction (achievable with good optimisation on a dense urban network) and a 20% time saving produces:
- Optimised distance: 147 km → fuel cost: €55.86
- Optimised time: 6 hours → driver cost: €132
- Optimised daily cost per van: €187.86/van/day
Saving per van per day: €56.94. For 10 vans over 250 working days: €142,350/year, from one API integration.
The benchmarks above reflect real published figures from last-mile logistics studies. Your specific numbers will vary by geography, vehicle type, and stop density. Dense urban areas see the largest gains because naive sequential routes waste the most distance on unnecessary backtracking.
Naive vs Optimised Routes: A Visual Comparison
The difference between a naive (sequential) route and an optimised one is stark on a map.

[Image: Two side-by-side map views of Amsterdam. Left map labelled "Sequential (naive)" shows a delivery route that zigzags across the city with visible backtracking, lines crossing over each other. Right map labelled "Optimised" shows the same 15 stops connected in a clean loop with no line crossings and a visibly shorter total path.]
Naive routing happens when you feed stops in the order they were entered, first customer who placed an order is first on the route, regardless of geography. In a city like Amsterdam or Berlin, this creates the "spaghetti route" problem: your driver constantly crossing their own path.
Optimisation solves the Travelling Salesman Problem (TSP) for your stop set. For 15–20 stops this is computationally tractable in milliseconds. For larger fleets with hundreds of stops, vehicle routing problem (VRP) solvers handle the additional constraints of multiple vehicles and capacity limits.
Step 1: Structure Your Delivery Data
Each stop needs a location and, for time-windowed deliveries, a time_window specifying when the delivery is acceptable.
import requests
import json
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.mapatlas.eu/v1"
# Depot (start and end point)
depot = {
"lat": 52.3402,
"lng": 4.8952,
"name": "Warehouse - Sloterdijk"
}
# Delivery stops with optional time windows
stops = [
{ "lat": 52.3726, "lng": 4.8971, "name": "Albert Heijn Jordaan",
"time_window": { "start": "09:00", "end": "12:00" } },
{ "lat": 52.3601, "lng": 4.9123, "name": "Café De Jaren",
"time_window": { "start": "08:00", "end": "11:00" } },
{ "lat": 52.3780, "lng": 4.8801, "name": "Westergasfabriek Events",
"time_window": { "start": "10:00", "end": "14:00" } },
{ "lat": 52.3545, "lng": 4.9041, "name": "Hotel V Nesplein",
"time_window": None },
{ "lat": 52.3620, "lng": 4.8820, "name": "Vondelpark Paviljoen",
"time_window": { "start": "07:00", "end": "10:00" } }
]
Step 2: Call the Route Optimisation Endpoint
POST the depot and stop list to the optimised routing endpoint. The API returns the stops in the most efficient visit order along with the total route distance and duration.
def optimise_route(depot, stops, vehicle_profile="van-euro6"):
"""
Request an optimised multi-stop route from the MapAtlas Routing API.
vehicle_profile options: van-euro6, van-diesel-euro5, electric-van, bike
"""
waypoints = [
{
"lat": s["lat"],
"lng": s["lng"],
"name": s["name"],
**({"time_window": s["time_window"]} if s.get("time_window") else {})
}
for s in stops
]
payload = {
"origin": { "lat": depot["lat"], "lng": depot["lng"] },
"destination": { "lat": depot["lat"], "lng": depot["lng"] }, # return to depot
"waypoints": waypoints,
"optimise": True,
"vehicle_profile": vehicle_profile,
"avoid_low_emission_zones": True # auto-avoids LEZs for non-compliant profiles
}
response = requests.post(
f"{BASE_URL}/routing/optimise",
json=payload,
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
)
response.raise_for_status()
return response.json()
result = optimise_route(depot, stops)
Step 3: Parse and Display the Optimised Route
The API response includes the stops in optimised order, cumulative ETAs for each stop, total distance, and total duration.
def display_route_summary(result):
route = result["route"]
print(f"\n--- Optimised Route Summary ---")
print(f"Total distance : {route['total_distance_km']:.1f} km")
print(f"Total duration : {route['total_duration_min']:.0f} min")
print(f"Stops : {len(route['waypoints'])}\n")
print(f" START {depot['name']}")
for i, stop in enumerate(route["waypoints"], 1):
eta = stop["eta"]
tw = stop.get("time_window")
on_time = "(on time)" if tw and tw["start"] <= eta <= tw["end"] else ""
print(f" {i:>2}. {stop['name']:<35} ETA {eta} {on_time}")
print(f" END {depot['name']}")
print(f"\nEstimated fuel saving vs sequential: "
f"{result.get('saving_vs_naive_km', 0):.1f} km "
f"({result.get('saving_pct', 0):.0f}%)")
display_route_summary(result)
Sample output for the five stops above:
--- Optimised Route Summary ---
Total distance : 38.4 km
Total duration : 94 min
Stops : 5
START Warehouse - Sloterdijk
1. Vondelpark Paviljoen ETA 07:48 (on time)
2. Café De Jaren ETA 08:31 (on time)
3. Albert Heijn Jordaan ETA 09:15 (on time)
4. Hotel V Nesplein ETA 10:02
5. Westergasfabriek Events ETA 10:44 (on time)
END Warehouse - Sloterdijk
Estimated fuel saving vs sequential: 14.2 km (27%)
Step 4: EU Low Emission Zone Handling
Amsterdam's ZTL zone, Paris's Crit'Air system, and Berlin's Umweltzone restrict certain vehicle types from central areas at specified times. A route that looks efficient on distance alone may be invalid for your vehicle.
The avoid_low_emission_zones: true parameter combined with the vehicle_profile automatically routes around restricted zones for non-compliant vehicles. For electric and Euro 6 vehicles, LEZs are passable and the parameter has no effect.
# Example: diesel Euro 5 van, will be re-routed around Amsterdam ZTL
result_euro5 = optimise_route(depot, stops, vehicle_profile="van-diesel-euro5")
# Example: electric van, LEZ restrictions do not apply
result_electric = optimise_route(depot, stops, vehicle_profile="electric-van")
print(f"Euro 5 route distance : {result_euro5['route']['total_distance_km']:.1f} km")
print(f"Electric route distance: {result_electric['route']['total_distance_km']:.1f} km")
# Electric route will typically be shorter as it can use LEZ-restricted roads
For logistics operations planning a transition from diesel to electric, comparing these two outputs per route provides a direct quantification of the range improvement available from electrification.
Step 5: Display the Optimised Route on a Map
Take the route geometry from the API response and render it as a line layer in JavaScript.
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
// routeResult is the parsed API JSON response passed to the frontend
function renderOptimisedRoute(map, routeResult) {
const { waypoints, geometry, total_distance_km, total_duration_min } = routeResult.route;
map.on('load', () => {
// Route line
map.addSource('optimised-route', { type: 'geojson', data: { type: 'Feature', geometry } });
map.addLayer({
id: 'route-line',
type: 'line',
source: 'optimised-route',
layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: { 'line-color': '#2563EB', 'line-width': 4 }
});
// Stop markers with sequence numbers
waypoints.forEach((stop, i) => {
const el = document.createElement('div');
el.textContent = i + 1;
el.style.cssText = `
width:28px;height:28px;border-radius:50%;background:#2563EB;color:#fff;
display:flex;align-items:center;justify-content:center;font-weight:700;
font-size:13px;border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,0.3)
`;
new mapmetricsgl.Marker({ element: el })
.setLngLat([stop.lng, stop.lat])
.setPopup(
new mapmetricsgl.Popup().setHTML(`
<strong>${i + 1}. ${stop.name}</strong>
<p>ETA: ${stop.eta}</p>
`)
)
.addTo(map);
});
// Fit map to route bounds
const coords = geometry.coordinates;
const bounds = coords.reduce(
(b, c) => b.extend(c),
new mapmetricsgl.LngLatBounds(coords[0], coords[0])
);
map.fitBounds(bounds, { padding: 48 });
// Summary panel
document.getElementById('route-summary').innerHTML = `
<strong>${total_distance_km.toFixed(1)} km</strong> ·
<strong>${total_duration_min.toFixed(0)} min</strong> ·
${waypoints.length} stops
`;
});
}
const map = new mapmetricsgl.Map({
container: 'route-map',
style: 'https://tiles.mapatlas.eu/styles/basic/style.json?key=YOUR_API_KEY',
center: [4.9041, 52.3676],
zoom: 12
});
renderOptimisedRoute(map, routeResult);

[Image: A map showing Amsterdam with a blue route line connecting five numbered circular markers in sequence. A summary bar above the map reads "38.4 km · 94 min · 5 stops". The route makes a clean loop with no visible backtracking.]
Calculating Your Real Savings
Once you have the API response in hand, the saving calculation is straightforward. The saving_vs_naive_km field in the response gives you distance saved directly. From that, derive cost savings:
def calculate_savings(result, fuel_cost_per_km=0.38, driver_cost_per_hour=22.0,
days_per_year=250, fleet_size=10):
saving_km = result.get("saving_vs_naive_km", 0)
saving_hours = saving_km / 50 # assume 50 km/h average
daily_fuel_saving = saving_km * fuel_cost_per_km
daily_driver_saving = saving_hours * driver_cost_per_hour
daily_total = daily_fuel_saving + daily_driver_saving
annual_fleet_saving = daily_total * days_per_year * fleet_size
print(f"Distance saved per route : {saving_km:.1f} km")
print(f"Time saved per route : {saving_hours * 60:.0f} min")
print(f"Daily saving (1 vehicle) : €{daily_total:.2f}")
print(f"Annual saving ({fleet_size} vehicles): €{annual_fleet_saving:,.0f}")
calculate_savings(result)
Time Window Optimisation
Delivering to a bakery at 06:00 and a restaurant at 14:00 while minimising total route distance is a constrained optimisation problem. The API handles this automatically, you only need to provide the windows:
# Time-sensitive stops, the API will schedule these within their windows
stops_with_windows = [
{ "lat": 52.3726, "lng": 4.8971, "name": "Bakery",
"time_window": { "start": "05:30", "end": "07:00" } },
{ "lat": 52.3620, "lng": 4.8820, "name": "Café",
"time_window": { "start": "07:00", "end": "09:00" } },
{ "lat": 52.3545, "lng": 4.9041, "name": "Restaurant",
"time_window": { "start": "13:00", "end": "15:00" } }
]
If any time window constraint cannot be satisfied given the depot departure time and current traffic model, the API returns a constraint_violations array listing which stops could not be reached on time. Your dispatch software can then alert the driver or suggest an earlier departure.
What to Build on Top of This
Route optimisation is the foundation. Once it is running, the natural extensions are:
- Live driver tracking: Feed optimised route coordinates to the Live Driver Tracking Map tutorial and show customers real-time ETA updates.
- Isochrone-based coverage planning: Use the Travel Time API to visualise which postcodes your fleet can reach within your delivery window. The Isochrone Maps Explained article shows how.
- Bulk address validation: Before running optimisation, validate all delivery addresses with the Geocoding API to catch typos and outdated postcodes. See How to Use the Geocoding API to Validate 10,000 Addresses in Bulk.
The Logistics and Delivery industry page and the Fleet Management industry page cover additional MapAtlas features relevant to dispatch software, including multi-vehicle VRP and return-to-depot optimisation.
Getting Started
- Sign up for a free MapAtlas API key, the free tier includes routing and optimisation calls, no credit card required
- Review the Routing API documentation for the full list of vehicle profiles, time window parameters, and multi-vehicle options
- Explore the Route Planning and Navigation capabilities page for a product overview
Frequently Asked Questions
How does route optimisation reduce delivery costs?
Route optimisation reorders multi-stop delivery sequences to minimise total distance and drive time. Studies consistently show 20–35% reductions in distance driven versus a naive sequential route. For a vehicle driving 200 km/day at €0.35/km fuel cost, a 30% reduction saves around €21 per vehicle per day, roughly €5,000 per year per vehicle.
What are time windows in route optimisation?
Time windows are delivery constraints that require a stop to be visited within a specified time range, for example, a business that accepts deliveries only between 09:00 and 12:00. The optimiser must respect all time windows while still minimising total route distance, which is a significantly harder computational problem than unconstrained optimisation.
Does the MapAtlas Routing API handle EU Low Emission Zones?
Yes. The MapAtlas Routing API includes road restriction data for EU Low Emission Zones including Amsterdam's ZTL, the Paris Crit'Air zone, and Berlin's Umweltzone. Pass the vehicle profile (diesel Euro 5, petrol, electric) as a parameter and the router will automatically avoid restricted zones for non-compliant vehicles.
