Split Orders

Why Does Order Splitting Matter?

When a single customer order exceeds the carrying capacity of any one vehicle in the fleet, the optimizer has only two options: assign the entire order to a vehicle that cannot physically carry it, or leave the order unassigned entirely. Neither outcome is useful when the ability to split an order is not available.

The Route Optimization API's Split Orders feature introduces a third option to allow the optimizer to divide a large order into sub-tasks, using the split configuration for each delivery/pickup type job. The order after splitting is distributed across multiple vehicles, all serving the same destination. Each sub-task is tracked with a unique split_indicator in the response, to reconstruct the full order breakdown from the optimized plan.

Use Case: Wholesale Bulk Goods Distribution

A wholesale distributor operates a depot in Los Angeles with a fleet of four trucks, each with a capacity of 10 units. Today's dispatch includes three customer orders:

  • Delivery 1: a 20-unit delivery
  • Delivery 2: a 30-unit delivery
  • Delivery 3: a standard 5-unit delivery

Both Delivery 1 and Delivery 2 exceed any single truck's capacity. The operations team wants the optimizer to:

  • Split Delivery 1 across up to 4 trucks, with each truck carrying at least 25% of the total (minimum 5 units per sub-task).

  • Split Delivery 2 across up to 5 trucks, with each truck carrying at least 33% of the total (minimum ~10 units per sub-task).

  • Assign Delivery 3 to a single truck without splitting.

  • Ensure that if any split job cannot be fulfilled completely using available trucks, the entire order should be left unassigned, ensuring no partial deliveries.

How Split Orders Works

Enabling a Split

To split a job it must be eligible for splitting along with information about the number of splits or load of each split.

  • Setting can_be_split: true on a job tells the optimizer it is permitted, but not required, to divide the job across multiple vehicles. The optimizer will split only when doing so improves the overall solution.

  • max_splits sets the upper bound on how many sub-tasks the job can be divided into. The optimizer may use fewer parts than this maximum if fewer vehicles are needed.

Controlling Sub-Task Size

The optional quant field expresses the minimum quantity each sub-task must carry as a fraction of the original job load. A value of 0.2 means each sub-task must deliver at least 20% of the total.

When quant is omitted, the solver splits greedily based on vehicle capacity i.e. each sub-task takes as much as the assigned vehicle can carry. This may result in highly uneven splits. Using quant prevents impractically small sub-tasks and keeps the delivery plan operationally meaningful.

Constraints and Eligibility Rules

  • Single-dimensional delivery/pickup jobs only: Split Orders apply only to jobs with a single load dimension i.e. a single-element delivery or pickup array. Multi-dimensional loads are not supported.

  • max_splits is mandatory: When can_be_split: true, the number of maximum splits allowed must be provided.

  • Shipments are not supported: The split configuration cannot be applied to shipment-type tasks.

Configuring the Feature: Input Parameters

The split configuration sits inside each job object. The table below summarises all available fields:

ParameterType / LocationDescription
jobs[].splitobject (job)Configuration object that enables and controls order splitting for this job.
split.can_be_splitbooleanSet to true to allow this job to be split across multiple vehicles.
split.max_splitsinteger (min: 2)The maximum number of sub-tasks this job can be divided into.
split.quantnumber (ratio)The minimum quantity allowed per split sub-task, expressed as a ratio of the original job's total load.

In our wholesale distribution example, the three jobs on input side are configured as follows:

JobTotal Loadcan_be_splitmax_splitsquant (ratio)Min per Split
Delivery 120 unitstrue40.25 (25%)5 units
Delivery 230 unitstrue50.33 (33%)~10 units
Delivery 35 unitsfalse (none)

Response: New Field Introduced

The table below summarises the new and modified fields that appear in the response for split jobs:

Response FieldTypeDescription
steps[].split_indicatorintegerA unique integer identifier assigned to each sub-task of a split job. Values range from 0 to (total parts − 1).

Example API Request & Response

Example API Request

The request below sets up four trucks, three jobs with different split configurations, and a duration-minimising objective:

1
curl --location 'https://api.nextbillion.io/optimization/v2?key=<your_api_key>' \
2
--header 'Content-Type: application/json' \
3
--data '{
4
"description": "Split Order Example",
5
"locations": {
6
"id": 1,
7
"location": [
8
"34.06584144,-118.27872390", // index 0 – Depot (all trucks start/end here)
9
"34.05260390,-118.29975983", // index 1 – Delivery 1 & 2 destination
10
"34.04173299,-118.27901708", // index 2 – Delivery 2 destination
11
"34.04586288,-118.25101803" // index 3 – Delivery 3 destination
12
]
13
},
14
"vehicles": [
15
{ "id": "Truck 1", "start_index": 0, "end_index": 0, "capacity": [10] },
16
{ "id": "Truck 2", "start_index": 0, "end_index": 0, "capacity": [10] },
17
{ "id": "Truck 3", "start_index": 0, "end_index": 0, "capacity": [10] },
18
{ "id": "Truck 4", "start_index": 0, "end_index": 0, "capacity": [10] }
19
],
20
"jobs": [
21
{
22
"id": "Delivery 1",
23
"location_index": 1,
24
"delivery": [20], // total load: 20 units
25
"split": {
26
"can_be_split": true,
27
"max_splits": 4, // up to 4 trucks can share this order
28
"quant": 0.25 // each part must be >= 25% of 20 = 5 units
29
}
30
},
31
{
32
"id": "Delivery 2",
33
"location_index": 2,
34
"delivery": [30], // total load: 30 units
35
"split": {
36
"can_be_split": true,
37
"max_splits": 5,
38
"quant": 0.33 // each part must be >= 33% of 30 = ~10 units
39
}
40
},
41
{
42
"id": "Delivery 3",
43
"location_index": 3,
44
"delivery": [5] // no split config: must be served whole
45
}
46
],
47
"options": { "objective": { "travel_cost": "duration" } }
48
}'

Example API Response

The condensed response shows routes for 3 trucks and 1 unassigned job. Look for split_indicator and the per-step delivery field on split job steps to reconstruct the full breakdown:

1
{
2
"description": "Split Order Example",
3
"result": {
4
"code": 0,
5
"summary": {
6
"cost": 4618, "routes": 3, "unassigned": 1,
7
"duration": 4618, "distance": 28336, "delivery": [25]
8
},
9
"unassigned": [
10
{
11
"id": "Delivery 2", "type": "job",
12
"reason": "cannot be served due to capacity constraint"
13
// quant=0.33 requires ~10 units per split; no valid truck combination available
14
}
15
],
16
"routes": [
17
{
18
"vehicle": "Truck 1", "cost": 1159,
19
"steps": [
20
{ "type": "start", "load": [10], "distance": 0 },
21
{ "type": "job", "id": "Delivery 1",
22
"split_indicator": 1, // part 1 of Delivery 1
23
"delivery": [10], // 10 of 20 units
24
"load": [0], "distance": 3330 },
25
{ "type": "end", "load": [0], "distance": 6591 }
26
],
27
"delivery": [10], "distance": 6591
28
},
29
{
30
"vehicle": "Truck 2", "cost": 1159,
31
"steps": [
32
{ "type": "start", "load": [5], "distance": 0 },
33
{ "type": "job", "id": "Delivery 1",
34
"split_indicator": 2, // part 2 of Delivery 1
35
"delivery": [5], // 5 of 20 units
36
"load": [0], "distance": 3330 },
37
{ "type": "end", "load": [0], "distance": 6591 }
38
],
39
"delivery": [5], "distance": 6591
40
},
41
{
42
"vehicle": "Truck 4", "cost": 2300,
43
"steps": [
44
{ "type": "start", "load": [10], "distance": 0 },
45
{ "type": "job", "id": "Delivery 3",
46
"load": [5], "distance": 3887 }, // no split: single delivery
47
{ "type": "job", "id": "Delivery 1",
48
"split_indicator": 0, // part 0 of Delivery 1
49
"delivery": [5], // 5 of 20 units
50
"load": [0], "distance": 11893 },
51
{ "type": "end", "load": [0], "distance": 15154 }
52
],
53
"delivery": [10], "distance": 15154
54
}
55
// Truck 3: not used
56
]
57
},
58
"status": "Ok"
59
}

Interpreting the Output

To find out a split order in the response, look for the split_indicator field. Its values range from 0 to (total parts − 1) and can be used to uniquely identify and track each sub-task of a split job.

Delivery 1: Successfully Split

Delivery 1 (20 units) was split into 3 sub-tasks across Trucks 1, 2, and 4. The table below shows the complete breakdown:

JobVehiclesplit_indicatorQuantity≥ Min (5)?Load After
Delivery 1Truck 405 units✅ Yes[0]
Delivery 1Truck 1110 units✅ Yes[0]
Delivery 1Truck 225 units✅ Yes[0]
Total3 vehicles0, 1, 220 units✅ Complete

All three sub-tasks satisfy the quant constraint (each ≥ 5 units = 25% of 20). The split_indicators 0, 1, 2 confirm three parts were created. The optimizer used 3 of the 4 available trucks to fulfill this delivery.

Delivery 2: Unassigned

Delivery 2 appears in the unassigned list with the reason cannot be served due to capacity constraint. The root cause is the interaction between quant and vehicles.capacity:

  • quant: 0.33 on a 30-unit order means each sub-task must carry at least 33% × 30 = ~10 units.

  • Since each truck has a total capacity of 10 units, the optimizer needs 3 trucks each fully dedicated to Delivery 2.

  • However, after allocating trucks to Delivery 1 and Delivery 3, no valid 3-truck combination remains free to serve Delivery 2 under these constraints.

Because all-or-nothing completion applies, Delivery 2 is dropped entirely rather than partially fulfilled.

Delivery 3: Standard Single-Vehicle Assignment

Delivery 3 has no split configuration and is assigned to Truck 4 as a standard single-vehicle job. It is served first on Truck 4's route before it also handles the Delivery 1 sub-task (split_indicator 0, 5 units) on the same run. This demonstrates that a split sub-task and a non-split job can coexist on the same vehicle.

What We Learned

This example illustrates several important takeaways about how Split Orders behaves in practice:

  • Enables fulfilment of orders larger than any single vehicle's capacity: The optimizer distributes sub-tasks across multiple trucks, making large orders routinely optimizable without manual pre-planning.

  • Split sub-tasks and regular jobs coexist freely: Truck 4 handled both Delivery 3, a standard delivery job, and a Delivery 1 sub-task in the same route. The optimizer fills remaining vehicle capacity with other work and split jobs do not monopolise the trucks assigned to them.

  • The optimizer may use fewer splits than max_splits allows: Delivery 1 was split into 3 parts despite a max_splits: 4 indicating that the optimizer creates only as many sub-tasks as needed for an efficient solution.

Explore other powerful features that the NextBillion.ai’s Route Optimization API can solve seamlessly.