La livraison du dernier kilomètre est la partie la plus coûteuse de toute chaîne d'approvisionnement. Les benchmarks sectoriels placent systématiquement les coûts du dernier kilomètre à 53 % du coût total d'expédition. Dans ce contexte, la plus grande variable contrôlable est l'efficacité des itinéraires. Un chauffeur qui effectue 15 arrêts dans le mauvais ordre peut parcourir 40 % de kilomètres en plus que nécessaire, brûlant du carburant, usant le véhicule et manquant des créneaux de livraison qui déclenchent des frais de relivraison.
L'optimisation des itinéraires n'est plus un problème difficile à résoudre en code. Ce qui nécessitait autrefois des logiciels de logistique spécialisés coûteux est désormais un appel API. Ce tutoriel construit un optimiseur d'itinéraires multi-arrêts complet avec l'API MapAtlas Routing : un script Python qui envoie une liste d'arrêts de livraison, récupère une séquence optimisée avec la distance et le temps totaux, applique des contraintes de fenêtres temporelles et gère les restrictions de zones à faibles émissions de l'UE pour les livraisons urbaines. Un extrait JavaScript affiche ensuite le résultat sur une carte.
L'implémentation Python tient en moins de 55 lignes. L'affichage de la carte JavaScript représente 30 lignes supplémentaires.
Le problème du coût du dernier kilomètre
Pour comprendre ce que l'optimisation économise réellement, calculons les chiffres pour un scénario de livraison réaliste :
- Flotte : 10 camionnettes
- Arrêts par camionnette par jour : 18
- Distance moyenne actuelle : 210 km/camionnette/jour
- Coût du carburant : 0,38 €/km (diesel, moyenne UE)
- Coût du chauffeur : 22 €/heure
- Temps de route moyen actuel : 7,5 heures/jour
Coût journalier actuel par camionnette : (210 × 0,38 €) + (7,5 × 22 €) = 79,80 € + 165 € = 244,80 €/camionnette/jour
Une réduction de distance de 30 % (réalisable avec une bonne optimisation sur un réseau urbain dense) et une économie de temps de 20 % donnent :
- Distance optimisée : 147 km, coût carburant : 55,86 €
- Temps optimisé : 6 heures, coût chauffeur : 132 €
- Coût journalier optimisé par camionnette : 187,86 €/camionnette/jour
Économie par camionnette par jour : 56,94 €. Pour 10 camionnettes sur 250 jours ouvrés : 142 350 €/an, grâce à une seule intégration API.
Les benchmarks ci-dessus reflètent de vraies données publiées dans des études de logistique du dernier kilomètre. Vos chiffres spécifiques varieront selon la géographie, le type de véhicule et la densité des arrêts. Les zones urbaines denses voient les gains les plus importants car les itinéraires séquentiels naïfs gaspillent le plus de distance en allers-retours inutiles.
Comparaison visuelle : itinéraires naïfs vs optimisés
La différence entre un itinéraire naïf (séquentiel) et un itinéraire optimisé est frappante sur une carte.
Le routage naïf se produit quand vous saisissez les arrêts dans l'ordre où ils ont été entrés : le premier client ayant passé commande est le premier sur l'itinéraire, indépendamment de la géographie. Dans une ville comme Amsterdam ou Berlin, cela crée le problème de "l'itinéraire spaghetti" : votre chauffeur croise constamment son propre chemin.
L'optimisation résout le Problème du Voyageur de Commerce (TSP) pour votre ensemble d'arrêts. Pour 15 à 20 arrêts, c'est calculable en quelques millisecondes. Pour les flottes plus importantes avec des centaines d'arrêts, les solveurs de problème de routage de véhicules (VRP) gèrent les contraintes supplémentaires de plusieurs véhicules et de limites de capacité.
Étape 1 : Structurer vos données de livraison
Chaque arrêt a besoin d'un emplacement et, pour les livraisons avec fenêtres temporelles, d'une time_window précisant quand la livraison est 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" } }
]
Étape 2 : Appeler l'endpoint d'optimisation d'itinéraire
Envoyez en POST le dépôt et la liste d'arrêts à l'endpoint de routage optimisé. L'API renvoie les arrêts dans l'ordre de visite le plus efficace avec la distance et la durée totales de l'itinéraire.
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)
Étape 3 : Analyser et afficher l'itinéraire optimisé
La réponse API inclut les arrêts dans l'ordre optimisé, les ETAs cumulatifs pour chaque arrêt, la distance totale et la durée totale.
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)
Exemple de sortie pour les cinq arrêts ci-dessus :
--- 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%)
Étape 4 : Gestion des zones à faibles émissions de l'UE
La zone ZTL d'Amsterdam, le système Crit'Air de Paris et l'Umweltzone de Berlin restreignent certains types de véhicules des zones centrales à des heures spécifiées. Un itinéraire qui semble efficace en termes de distance peut être invalide pour votre véhicule.
Le paramètre avoid_low_emission_zones: true combiné avec le vehicle_profile achemine automatiquement autour des zones restreintes pour les véhicules non conformes. Pour les véhicules électriques et Euro 6, les ZFE sont accessibles et le paramètre n'a aucun effet.
# 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
Pour les opérations logistiques qui planifient une transition du diesel à l'électrique, comparer ces deux sorties par itinéraire fournit une quantification directe de l'amélioration de portée disponible grâce à l'électrification.
Étape 5 : Afficher l'itinéraire optimisé sur une carte
Récupérez la géométrie de l'itinéraire depuis la réponse API et affichez-la comme couche de ligne en 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);
Calcul de vos économies réelles
Une fois la réponse API en main, le calcul des économies est simple. Le champ saving_vs_naive_km dans la réponse vous donne directement la distance économisée. À partir de là, dérivez les économies de coûts :
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)
Optimisation des fenêtres temporelles
Livrer à une boulangerie à 06:00 et à un restaurant à 14:00 tout en minimisant la distance totale est un problème d'optimisation sous contraintes. L'API gère cela automatiquement : vous n'avez qu'à fournir les fenêtres :
# 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" } }
]
Si une contrainte de fenêtre temporelle ne peut être satisfaite compte tenu de l'heure de départ du dépôt et du modèle de trafic actuel, l'API renvoie un tableau constraint_violations listant les arrêts qui n'ont pas pu être atteints à temps. Votre logiciel de dispatch peut alors alerter le chauffeur ou suggérer un départ plus tôt.
Ce que vous pouvez construire par-dessus
L'optimisation des itinéraires est la fondation. Une fois qu'elle fonctionne, les extensions naturelles sont :
- Suivi en direct du chauffeur : Envoyez les coordonnées d'itinéraire optimisé au tutoriel de carte de suivi en direct du chauffeur et montrez aux clients des mises à jour ETA en temps réel.
- Planification de couverture basée sur les isochrones : Utilisez l'API Travel Time pour visualiser quels codes postaux votre flotte peut atteindre dans votre fenêtre de livraison. L'article Cartes isochrones expliquées montre comment.
- Validation d'adresses en masse : Avant de lancer l'optimisation, validez toutes les adresses de livraison avec l'API de géocodage pour détecter les fautes de frappe et les codes postaux obsolètes. Voir Comment utiliser l'API de géocodage pour valider 10 000 adresses en masse.
La page secteur Logistique et Livraison et la page secteur Gestion de flotte couvrent des fonctionnalités MapAtlas supplémentaires pertinentes pour les logiciels de dispatch, notamment le VRP multi-véhicules et l'optimisation de retour au dépôt.
Pour commencer
- Créez une clé API MapAtlas gratuite : le tier gratuit inclut les appels de routage et d'optimisation, sans carte de crédit requise
- Consultez la documentation de l'API Routing pour la liste complète des profils de véhicules, des paramètres de fenêtres temporelles et des options multi-véhicules
- Explorez la page des capacités de planification d'itinéraires et de navigation pour une vue d'ensemble du produit
Questions fréquentes
Comment l'optimisation des itinéraires réduit-elle les coûts de livraison ?
L'optimisation des itinéraires réorganise les séquences de livraison multi-arrêts pour minimiser la distance totale et le temps de conduite. Les études montrent systématiquement des réductions de 20 à 35 % de la distance parcourue par rapport à un itinéraire séquentiel naïf. Pour un véhicule parcourant 200 km/jour à 0,35 €/km de carburant, une réduction de 30 % économise environ 21 € par véhicule par jour, soit environ 5 000 € par an par véhicule.
Que sont les fenêtres temporelles en optimisation des itinéraires ?
Les fenêtres temporelles sont des contraintes de livraison qui exigent qu'un arrêt soit visité dans une plage horaire spécifiée, par exemple, une entreprise qui n'accepte les livraisons qu'entre 09:00 et 12:00. L'optimiseur doit respecter toutes les fenêtres temporelles tout en minimisant la distance totale de l'itinéraire, ce qui est un problème de calcul nettement plus difficile que l'optimisation sans contraintes.
L'API MapAtlas Routing gère-t-elle les zones à faibles émissions de l'UE ?
Oui. L'API MapAtlas Routing inclut des données de restriction routière pour les zones à faibles émissions de l'UE, y compris la ZTL d'Amsterdam, la zone Crit'Air de Paris et l'Umweltzone de Berlin. Passez le profil de véhicule (diesel Euro 5, essence, électrique) comme paramètre et le routeur évitera automatiquement les zones restreintes pour les véhicules non conformes.

