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.