The phrase "nearest courier assignment" shows up in almost every dispatch tool pitch deck. It sounds like a solved problem. You have a list of couriers, you have a store location, you pick the one closest to the store. What's hard about that?
Quite a lot, as it turns out. In a dense Sao Paulo retail district where five couriers are within 400 meters of a pickup counter at 12:30 pm, nearest-courier assignment in its naive form produces worse outcomes than a moderately informed human dispatcher. This post covers the specific failure modes and how our routing layer addresses them.
Why Nearest-Courier Fails in Dense Urban Retail
The first problem is zone overlap. In a retail district with high courier density, multiple couriers are legitimately within range of multiple stores simultaneously. Naive nearest-courier assignment can route all available couriers to a single high-volume store during its peak window, leaving adjacent stores without coverage. From any individual store's perspective, the assignment looked correct. From the network perspective, you've created a coverage hole that compounds across the next 20 minutes.
The second problem is ETA versus proximity. Courier distance from a store is not the same as courier ETA. A courier 200 meters away on a one-way street grid in Vila Madalena may have a 6-minute ETA because of the routing path. A courier 350 meters away on a direct avenue may arrive in 3 minutes. Naive nearest-courier using straight-line distance produces systematically wrong ETAs in Sao Paulo's street geometry.
The third problem is courier load state. A courier already carrying two active deliveries should not be assigned a third at the same priority as an idle courier. Ignoring load state when assigning orders creates a bimodal courier utilization distribution: some couriers are always overloaded, some are always idle, and average utilization across the pool stays well below optimal.
How Zone-Based Routing Actually Works
Our routing layer starts with a zone graph rather than a flat courier list. Each store defines three to six pickup zones based on physical geography: curbside A, curbside B, interior counter, adjacent street parking slots. Each zone has a set of courier approach paths, based on actual street geometry, and a default capacity (how many active couriers it can accommodate simultaneously without creating a physical congestion point).
When a new order arrives, the system doesn't ask "which courier is nearest." It asks: which zone does this order dispatch from, what is the zone's current capacity utilization, and which available courier has the lowest weighted cost to reach that zone within the order's ETA window?
The cost function weights three inputs: network-routed ETA (not straight-line distance), courier load state (active deliveries plus pending assignments), and zone congestion score. Here is a simplified view of how that scoring looks for a four-courier assignment decision at a single store:
Courier Assignment Scoring: Order #3014, Zone B Curbside
| Courier | Routed ETA | Active Load | Zone Fit | Cost Score |
|---|---|---|---|---|
| C-07 | 2 min | 0 active | Optimal | 1.4 |
| C-12 | 1.5 min | 2 active | Good | 3.2 |
| C-03 | 4 min | 1 active | Good | 4.8 |
| C-19 | 3 min | 3 active | Overloaded | 8.7 |
C-07 wins despite C-12's shorter ETA. C-12's load state penalty outweighs the 30-second ETA advantage.
Notice that C-12, the naively nearest courier at 1.5 minutes, is not selected. The load penalty from two active deliveries pushes its cost score above C-07, which is idle and 2 minutes out. In practice, assigning a third delivery to C-12 would mean one of those deliveries gets delayed, creating a downstream wait-time problem for a different customer.
The Batching Decision
Batching, sending one courier to pick up multiple orders in sequence, adds another layer of complexity. Done well, batching improves courier utilization and reduces total vehicle trips. Done poorly, it increases customer wait time for every order in the batch, and the last order in the sequence waits the longest.
Our batching logic applies a simple constraint: a batch is only considered if all orders in the batch share the same dispatch zone and all are in ready or near-ready status. We don't batch across zones and we don't batch an order that's still in picking against one that's bagged. Those two constraints eliminate the most common batching failure mode, where an otherwise-efficient batch is held up because one item in it is running 8 minutes late.
For quick-commerce contexts where speed is the primary SLA, we default to single-order dispatch unless the store explicitly enables batching. Batching for speed is a contradiction in terms. It makes sense for stores optimizing courier utilization over individual order speed, typically in lower-urgency fulfillment windows.
Real-Time Resequencing
Assignment decisions are made at intake, but they're revised continuously. If a courier who was 2 minutes out gets stalled in traffic at Avenida Paulista and their ETA updates to 9 minutes, the system doesn't lock that assignment in. It evaluates the new ETA against the remaining available couriers and may reassign if a better option exists and the order hasn't been confirmed for pickup yet.
This resequencing creates a visibility challenge for counter staff: an order can change its assigned courier while it's in picking. Our dispatch view handles this by showing the confirmed courier only when the assignment is locked (typically when the courier is within 90 seconds of arrival). Before that, staff see the order status and estimated ready time, not a specific courier name. That prevents the "where is courier X?" question when X has just been reassigned.
What This Approach Does Not Handle Well
We want to be clear about the limits of the current model. It works well for courier-based last-mile dispatch in retail pickup contexts. It does not handle multi-stop route optimization for couriers doing sequential home deliveries, and it doesn't model traffic conditions beyond the real-time ETA signals we receive from the courier's location data. A courier who has been sitting in the same block for 10 minutes is flagged as a potential issue, but the system doesn't know whether that's a traffic jam, a long pickup, or an abandoned delivery.
It also assumes the courier pool is drawn from a gig-style dispatch model where couriers are available for reassignment between pickups. Stores using contracted delivery staff with fixed routes need a different configuration, and we handle that as a custom setup rather than a default.
The routing layer is the part of the system that changes most between store configurations. Zone counts, street geometry, courier pool behavior, and order mix all vary enough that a single fixed algorithm performs poorly across all of them. What we've built is a configurable framework with sensible defaults, not a one-size dispatches-all solution.