Packing recipes: one technique at a time, each a real declared field.
Each recipe answers one narrow question with the smallest real request that shows it - not a full scenario, just the field that does the work.
Pick the cheapest box that still fits
More than one candidate box is available, and the cheapest one that actually fits should win - not the first one that looks big enough.
Set configuration.objective to lowest_cost and give each container its own cost_minor. The objective ranks by cost only among arrangements that are geometrically feasible - it never trades a fit for a lower price.
const request = {
configuration: { objective: 'lowest_cost' },
items: [{ id: 'sku', quantity: 6, weight: '2 kg',
dimensions: { length: '200', width: '150', height: '100' } }],
containers: [
{ id: 'small', cost_minor: 120, inner_dimensions: { length: '300', width: '200', height: '200' } },
{ id: 'large', cost_minor: 260, inner_dimensions: { length: '600', width: '400', height: '400' } },
],
};The engine opens the small box whenever the six units actually fit inside it - cost only breaks a tie between arrangements that both work, never a substitute for checking that they do.
Split one SKU across as many cartons as it needs
One SKU, a quantity larger than a single carton holds, and the real question is how many cartons that quantity actually needs.
Declare the item once with its full quantity, and give the container its own available quantity. Both expand into that many physical instances for the search, so changing carton size is a data change, not a code change.
$items = [Item::create('bin', Dimensions::mm('300', '200', '200'),
weight: '4 kg', quantity: 40)];
$containers = [Container::create('crate', Dimensions::mm('600', '400', '400'),
maxPayload: '80 kg', quantity: 20)];The result's container count is the answer: exactly how many crates the 40 bins need, not an estimate to double-check by hand.
Keep a fragile item unstacked
A fragile item must never have anything resting on top of it, no matter how much room is left in the container.
Set stackable to false (or max_top_load to zero) on the item. The search then never places another item above it, even where the geometry would otherwise allow it.
Item.create(
"glassware", Dimensions.mm("200", "200", "250"), weight="5 kg",
stackable=False, max_top_load="0 kg",
)Any item that would otherwise stack on the glassware is placed elsewhere in the container instead, or in another container if nowhere else has room.
Keep two SKUs from ever sharing a container
Two products that a compatibility policy keeps apart, even when there's plenty of room for both in the same container.
Tag one item's incompatible_tags with the tag the other item carries. Packing them together then stops being a candidate the search considers at all, regardless of leftover space.
Item.create("bleach", Dimensions.mm("150", "150", "300"), weight="3 kg",
tags=("hazmat",), incompatible_tags=("food",))
Item.create("flour", Dimensions.mm("300", "200", "150"), weight="2 kg",
quantity=4, tags=("food",))The engine opens a second container on its own rather than ever proposing an arrangement that puts the two together - the rule is enforced during the search, not checked afterwards.
Model a wheel arch or roof shelf
A vehicle's usable volume isn't a clean rectangular box - a wheel arch, a support post or a roof shelf eats into it.
Declare the intrusion as an obstacle on the container, with its own origin and dimensions. Placements are routed around it exactly like they would route around another item.
containers: [{
id: 'van', inner_dimensions: { length: '1200', width: '600', height: '600' },
obstacles: [
{ id: 'wheel-arch', origin: { x: '900', y: '0', z: '0' },
dimensions: { length: '300', width: '150', height: '200' } },
],
}]A crate that would otherwise land inside the arch's footprint is shifted sideways to clear it - verified by packing four identical crates and watching the fourth move off the row the first three follow.
Keep a two-axle load compliant
A vehicle can be within its total payload limit and still exceed a declared axle limit, because one axle alone carries more than its own limit allows.
Declare both axles' position and max_load on the container. Axle compliance then becomes a constraint the search enforces while placing items, not a spreadsheet check run on the result afterwards.
containers: [{
id: 'trailer', inner_dimensions: { length: '2000', width: '200', height: '200' },
axles: [
{ position: '400', max_load: '300 kg' },
{ position: '1600', max_load: '300 kg' },
],
}]An item that would push either axle reaction past its declared limit is refused, even in a trailer with plenty of spare payload and floor space - the refusal is about weight distribution, not volume.
Compute a safe unload sequence
A tightly packed load is only useful if each item can actually come back out in the order the route needs.
Pack normally, then feed the resulting placements into placementReachability (or safeRemovalOrder) alongside each item's stop. It reports which items are reachable right now, and which are blocked, and by what.
const reach = placementReachability(boxes, containerDims, stops);
// [{ index: 0, reachable: true, ... },
// { index: 1, reachable: false, blocked_by_route: [0], ... }]The crate due at the first stop reports reachable, the one due later reports blocked by the first one's index - not a generic "cannot unload," a named cause.
Cap a pallet's stack within its own crush rating
A pallet's height allows many layers, but the cartons themselves can safely bear only so much weight stacked on top of them.
Set maxStackedItems on the carton. The search caps every column at that many units on top of the base, regardless of how much vertical room the pallet still has. The carton is also kept upright here: stood on end, two columns would fit the footprint and the limit would never bind.
Item::create('carton', Dimensions::mm('400', '300', '200'),
weight: '8 kg', quantity: 6, maxStackedItems: 2, keepUpright: true);A column stops at one base carton plus two stacked on it, even though the pallet is tall enough for six - the fourth carton in that column comes back unpacked rather than crushing the ones below it.
Let a load overhang the pallet by a set margin
Cartons a little larger than half the deck leave two to a layer on the pallet itself, although your carrier accepts a small overhang that would take four.
Give the pallet container a pallet_overhang_limit: how far the load may pass the deck at each end (length) and at each side (width). It is permission, not a target - a load the deck holds is never overhung - and only the deck bears weight. Write the rule your carrier or standard states; zero forbids any overhang.
{ id: 'euro-pallet', quantity: 1, max_payload: '1000 kg',
inner_dimensions: { length: '1200', width: '800', height: '650' },
pallet_overhang_limit: { length: '15', width: '15' } }Eight 610 x 410 mm cartons all go on, four to a layer, and the pallet reports achieved_dimensions of 1220 x 820 x 600 mm - the size it is billed and handed on at. Without the limit only four fit and the other four come back unpacked.
Ship flat goods in a gusseted mailer
A poly mailer with side gussets holds more than a flat one of the same size, and a box model of either gets the answer wrong.
Declare the mailer with deformable_boundary kind gusseted_bag and its side_gusset_depth. The bag's width girth grows by the gusset depth - the width plus the stack's thickness may reach the width plus the gusset - and every other rule stays the flat bag's.
{ id: 'pouch', quantity: 1,
inner_dimensions: { length: '300', width: '150', height: '100' },
deformable_boundary: { kind: 'gusseted_bag', side_gusset_depth: '25' } }Three 200 x 100 x 20 mm books fit the gusseted pouch; declared as kind flat_bag, the same pouch takes two, and the rest come back as container_inventory_exhausted.
Keep loaded items where they are
Part of the load is already on board when the plan changes, and a fresh solve is free to put those boxes somewhere else.
List what is already loaded under fixed_placements: the item type, the container instance, the position the earlier result reported and its orientation. The solve packs everything else around them.
fixed_placements: [{
item_type: 'crate', container_type: 'van', container_instance: 1,
position: { x: '600', y: '0', z: '0' }, orientation: 'LWH',
}]The crate fixed at x = 600 mm comes back there, marked fixed: true, and the other three are packed around it. A crate fixed so that it sticks out of the van is refused before any search, as invalid_fixed_placement: outside_container: crate#1 - the same words from the JavaScript, Python and PHP engines.
Pack units into cartons, then cartons onto a pallet
Fulfilment is rarely one level: units go into cartons and the cartons go onto a pallet, and solving the two separately means carrying sizes and weights across by hand.
Call packNested with one ordinary request per level. Each packed container of a level is handed to the next as an item: upright, at its outer_dimensions and its gross weight, named by its container id. A level that lists no items packs what the level before it handed up.
import { packNested } from '@packvium/engine';
const { levels } = packNested({ levels: [
{ name: 'carton', request: {
items: [{ id: 'jar', quantity: 24, weight: '400 g',
dimensions: { length: '100', width: '100', height: '120' } }],
containers: [{ id: 'carton-12', tare_weight: '300 g',
inner_dimensions: { length: '400', width: '300', height: '120' },
outer_dimensions: { length: '410', width: '310', height: '130' } }],
} },
{ name: 'pallet', request: {
items: [],
containers: [{ id: 'pallet', quantity: 1, max_payload: '500 kg',
inner_dimensions: { length: '1200', width: '800', height: '1000' } }],
} },
] });The carton level opens two cartons of twelve jars. The pallet level receives them as carton-12#1 and carton-12#2, each 410 mm long - the outer size, not the inner one - and places both on the one pallet.
Cut a box to the order instead of choosing one
A box-making machine cuts each carton to the order, so there is no list of stock sizes to choose from - only the largest box the machine can make and the grid it cuts on.
Give the container a made_to_measure block: the minimum box and the increment per side. Its inner_dimensions then state the largest box the machine can cut. Add cost_per_square_metre_minor and the board each box uses is priced into the score.
{ id: 'cut-box', cost_minor: 20,
inner_dimensions: { length: '800', width: '600', height: '600' },
made_to_measure: {
minimum: { length: '150', width: '100', height: '50' },
increment: { length: '10', width: '10', height: '10' },
cost_per_square_metre_minor: 90,
} }A 310 x 205 x 447 mm lamp and a 300 x 300 x 180 mm shade come back in one box whose inner_dimensions are 450 x 310 x 390 mm: the load's own 447 x 310 x 385 mm extent, reported as achieved_dimensions, rounded up onto the 10 mm grid. On a 25 mm grid the same order is cut at 450 x 325 x 400 mm.
Cap the search and keep the answer repeatable
A time limit stops a long search, but where it stops depends on how fast the machine is, so two runs of one request can disagree.
Set configuration.effort_budget: max_candidates_evaluated, max_placement_attempts, max_search_nodes or max_restarts. It counts work rather than milliseconds, so the search is cut at the same point wherever it runs. Keep time_limit_ms as the fuse behind it.
configuration: {
solver_profile: 'quality',
effort_budget: { max_candidates_evaluated: 200 },
}All seventy cartons still come back placed in one crate, and the result says how the search ended: termination.code is effort_limit and algorithm.effort_limit_reached is true, so this is the best answer found inside the budget, not a finished search. Run the request again and the placements are the same.
Read why an item was refused
An order comes back incomplete, and 'it did not fit' is not something you can act on or tell a customer.
Read unpacked_items. Every refused unit carries a reason code and a proof level, and proven means the request itself rules the item out rather than the search running short. explainUnpackedItem turns the entry into a sentence.
import { explainUnpackedItem, pack } from '@packvium/engine';
const result = pack(request);
for (const refused of result.unpacked_items) {
console.log(refused.reason, refused.proof.level, explainUnpackedItem(refused));
}The two mugs are packed and the status is best_found. The 40 kg anvil is refused as payload_exceeded and the 900 mm poster tube as no_compatible_container_dimensions, both proven - so the fix is a different box, not a longer search.
Pack in fractional inches without rounding
Dimensions arrive as 12 3/8 in, and converting them to decimal millimetres first rounds away the fraction that decides whether a tight fit is a fit.
Set units.length to in and write each length as a string, whole number and fraction together. Lengths are carried as exact integer ticks, so nothing is rounded on the way in or on the way out.
units: { length: 'in' },
items: [{ id: 'plank', quantity: 2,
dimensions: { length: '12 3/8', width: '8 1/2', height: '3/4' } }],
containers: [{ id: 'sleeve',
inner_dimensions: { length: '12 3/8', width: '8 1/2', height: '1 1/2' } }],Two 3/4 in planks fill the 1 1/2 in sleeve exactly: the status is optimal and the second plank sits at z = 0.75 in. Make the sleeve 1 31/64 in tall - one sixty-fourth short - and each plank needs a sleeve of its own.
Declare an item that does not fill its box
Two wedges cut from one block nest into a single crate, but packed as their bounding boxes each one claims a crate to itself.
Set shape_type to convex_hull and list hull_vertices in the item's own coordinates. The declared dimensions still bound the item; the hull says how much of that box is solid, and collisions are decided on the vertices in exact integers.
{ id: 'wedge-lower', quantity: 1, weight: '1 kg',
dimensions: { length: '100', width: '100', height: '100' },
shape_type: 'convex_hull',
hull_vertices: [
{ x: '0', y: '0', z: '0' }, { x: '100', y: '0', z: '0' },
{ x: '0', y: '100', z: '0' }, { x: '0', y: '0', z: '100' },
{ x: '100', y: '0', z: '100' }, { x: '0', y: '100', z: '100' },
] }Packed as plain cuboids, the two wedges open two 100 mm crates. Declared as hulls they are complementary halves of one cube and share a single crate.
Run your own request, or read the full field list.
The playground runs a real request without installing anything. The request contract page lists every field these examples use, and the ones they don't.
Try in PlaygroundRead the full contract