Build a delivery zone map
A delivery area is not a circle. A circle drawn at five kilometres crosses the river, the rail yard and the escarpment as if they were not there, and it promises delivery to an address your driver needs forty minutes to reach. What you want is the set of places reachable in a given time along real roads, which is an isochrone.
This builds three zones around a depot, draws them, and tests a customer address against them. The code is plain TypeScript. For the React, Next.js, Vue or Nuxt wiring, start from the framework recipes and paste these calls into the component.
Find the depot and draw the zones
Start from an address rather than coordinates you looked up by hand, so the same code still works
when the depot moves. One isochrone call then returns all three bands as a GeoJSON
FeatureCollection, ordered outermost first.
const [depot] = await unmap.geocoder.search("1010 8 St SW, Calgary", { limit: 1 });
if (!depot) throw new Error("depot address not found");
const zones = await unmap.router.isochrone([depot.lng, depot.lat], {
minutes: [10, 20, 30],
mode: "auto",
});Longitude first, everywhere. If your zones appear in the Indian Ocean, that is why.
Four contours is the ceiling and 120 minutes is the longest one. Both are the gateway
mirroring Valhalla's own service limits, so asking for more is a 400 naming the rule rather than
a silent truncation. Each contour is a separate costing pass in the routing container, which is
why the cap exists at all.
For a cycling courier pass mode: "bicycle", and for a van with height or weight limits
mode: "truck" with a vehicle profile. The routing docs list all five modes.
Put them on the map
The response is already the shape MapLibre wants, so it goes in as a source untouched. Fill first and outline second keeps the boundaries legible where bands overlap.
import type { FeatureCollection } from "@unmap/routing";
function drawZones(zones: FeatureCollection) {
unmap.map!.on("load", () => {
unmap.map!.addSource("zones", { type: "geojson", data: zones });
unmap.map!.addLayer({
id: "zones-fill",
type: "fill",
source: "zones",
paint: { "fill-color": "#FF3E9A", "fill-opacity": 0.15 },
});
unmap.map!.addLayer({
id: "zones-line",
type: "line",
source: "zones",
paint: { "line-color": "#FF3E9A", "line-width": 1 },
});
});
}Test an address against a zone
There is no endpoint that answers "is this address in my delivery area", and there should not be: the answer depends on your business rules, not on the road network. Geocode the address, then ask the map which band the point lands in.
async function bandFor(address: string): Promise<number | undefined> {
const [customer] = await unmap.geocoder.search(address, { limit: 1 });
if (!customer) return undefined;
const pixel = unmap.map!.project([customer.lng, customer.lat]);
const [hit] = unmap.map!.queryRenderedFeatures(pixel, { layers: ["zones-fill"] });
return hit?.properties?.contour as number | undefined;
}contour is the band's own minute value, so this returns the drive-time bracket rather than a
bare yes or no. It reads the rendered map, which means it answers only for zones currently drawn
and only in the browser. For the same test on a server, keep the FeatureCollection and run a
point-in-polygon check against it with a geometry library.
What these zones are and are not
An isochrone is a model of a road network, not a promise about a Tuesday afternoon. It knows nothing about your loading bay, the driver's break, the school zone at 3pm, or live traffic, because the routing graph carries no live traffic at all. Treat the bands as the shape of your service area and keep the promise you make to a customer a business decision on top of it.
Coverage stops at the border. A depot near Windsor gets a zone that stops dead at the river rather than reaching into Detroit, because the routing graph is the Canada extract.
Related
- Travel-time areas for the smallest possible version
- Routing API for modes, truck profiles, and the full parameter list
- Frameworks for wiring this into React, Next.js, Vue or Nuxt