라스트마일 배송은 모든 공급망의 가장 비싼 부분입니다. 업계 벤치마크에 따르면 라스트마일 비용은 전체 배송 비용의 53%를 차지합니다. 이 중에서 가장 제어 가능한 변수는 경로 효율성입니다. 운전자가 15개의 정차를 잘못된 순서로 완료하면 필요한 것보다 40% 이상 더 많은 킬로미터를 운전할 수 있으며, 이는 연료를 소모하고 차량을 손상시키며 배송 재배송 수수료를 발생시키는 시간 창을 놓칩니다.
경로 최적화는 더 이상 코드로 해결하기 어려운 문제가 아닙니다. 과거에는 비싼 전문 물류 소프트웨어가 필요했지만 이제는 API 호출입니다. 이 튜토리얼은 MapAtlas Routing API를 사용하는 완전한 다중 정차 경로 최적화 프로그램을 구축합니다: 배송 정차 목록을 보내고 총 거리와 시간이 있는 최적화된 시퀀스를 받으며, 시간 창 제약을 적용하고, 도시 배송을 위한 EU 저배출 구역 제한을 처리하는 Python 스크립트입니다. 그런 다음 JavaScript 코드 조각이 결과를 지도에 그립니다.
Python 구현은 55줄 미만입니다. JavaScript 지도 표시는 추가로 30줄입니다.
라스트마일 비용 문제
최적화가 실제로 절감되는 비용을 이해하려면 현실적인 배송 시나리오에 대한 숫자를 계산해보세요:
- 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.
순진한 경로 vs 최적화된 경로: 시각적 비교
순진한(순차적) 경로와 최적화된 경로의 차이는 지도에서 명백합니다.
순진한 경로는 입력된 순서대로 정차를 공급할 때 발생하며, 주문을 먼저 한 첫 번째 고객이 지리와 관계없이 경로의 첫 번째입니다. 암스테르담이나 베를린 같은 도시에서 이것은 '스파게티 경로' 문제를 만듭니다: 운전자가 지속적으로 자신의 경로를 교차합니다.
최적화는 정차 집합에 대해 여행 판매원 문제(TSP)를 해결합니다. 15-20개의 정차의 경우 이는 밀리초 단위로 계산 가능합니다. 수백 개의 정차가 있는 더 큰 함대의 경우 차량 경로 문제(VRP) 솔버가 여러 차량과 용량 제한이라는 추가 제약을 처리합니다.
1단계: 배송 데이터 구조화
각 정차에는 위치가 필요하며, 시간 창이 있는 배송의 경우 배송이 수용 가능한 시기를 지정하는 time_window가 필요합니다.
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" } }
]
2단계: 경로 최적화 엔드포인트 호출
최적화된 경로 끝점에 창고 및 정차 목록을 게시합니다. API는 총 경로 거리 및 지속 시간과 함께 가장 효율적인 방문 순서로 정차를 반환합니다.
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)
3단계: 최적화된 경로 파싱 및 표시
API 응답에는 최적화된 순서의 정차, 각 정차의 누적 ETA, 총 거리 및 총 지속 시간이 포함됩니다.
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%)
4단계: EU 저배출 구역 처리
암스테르담의 ZTL 지역, 파리의 Crit'Air 시스템, 베를린의 Umweltzone은 특정 시간에 특정 차량 유형이 중앙 지역에 접근하는 것을 제한합니다. 거리만으로 효율적으로 보이는 경로는 귀하의 차량에 유효하지 않을 수 있습니다.
avoid_low_emission_zones: true 매개변수와 vehicle_profile을 결합하면 규정을 준수하지 않는 차량의 제한된 지역 주위로 자동으로 경로를 지정합니다. 전기 및 Euro 6 차량의 경우 LEZ는 통과 가능하며 매개변수는 효과가 없습니다.
# 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.
5단계: 지도에 최적화된 경로 표시
API 응답에서 경로 기하학을 가져와 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);
실제 절감액 계산
API 응답을 받으면 절감액 계산은 간단합니다. 응답의 saving_vs_naive_km 필드는 직접 절감된 거리를 제공합니다. 여기에서 비용 절감액을 도출합니다:
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)
시간 창 최적화
총 경로 거리를 최소화하면서 06:00에 베이커리로, 14:00에 레스토랑으로 배송하는 것은 제약이 있는 최적화 문제입니다. API는 이를 자동으로 처리하므로 창만 제공하면 됩니다:
# 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" } }
]
창고 출발 시간 및 현재 트래픽 모델이 주어진 상황에서 시간 창 제약을 만족할 수 없다면 API는 시간 내에 도달할 수 없는 정차를 나열하는 constraint_violations 배열을 반환합니다. 디스패치 소프트웨어는 운전자에게 경고하거나 더 빠른 출발을 제안할 수 있습니다.
이 위에 구축할 것
경로 최적화는 기초입니다. 실행되면 자연스러운 확장은 다음과 같습니다:
- 실시간 운전자 추적: 최적화된 경로 좌표를 Live Driver Tracking Map tutorial로 공급하고 고객에게 실시간 ETA 업데이트를 표시합니다.
- 등시선 기반 커버리지 계획: Travel Time API를 사용하여 배송 기간 내에 함대가 도달할 수 있는 우편 번호를 시각화합니다. Isochrone Maps Explained 문서에서 방법을 확인하세요.
- 대량 주소 검증: 최적화를 실행하기 전에 지오코딩 API로 모든 배송 주소를 검증하여 오타 및 오래된 우편 번호를 잡습니다. How to Use the Geocoding API to Validate 10,000 Addresses in Bulk를 참조하세요.
Logistics and Delivery industry page 및 Fleet Management industry page는 다중 차량 VRP 및 창고 복귀 최적화를 포함하여 디스패치 소프트웨어와 관련된 추가 MapAtlas 기능을 다룹니다.
시작하기
- 무료 MapAtlas API 키에 등록하세요, 무료 계층에는 경로 지정 및 최적화 호출이 포함되며 신용 카드는 필요하지 않습니다.
- 차량 프로필, 시간 창 매개변수 및 다중 차량 옵션의 전체 목록에 대해 Routing API 문서를 검토하세요.
- 제품 개요를 위해 Route Planning and Navigation capabilities page를 탐색하세요.
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.

