Zone crossing penalization

This guide covers zone crossing penalization in JOpt.TourOptimizer: the cost it adds when a route moves from one zone into another, the global multiplier, the multipliers per direction and the example with Manhattan and Jersey City. It is for developers who plan routes in areas split by bridges, tunnels, toll borders or congestion zones, in Java or through the REST service. It builds on the zone qualifications of nodes from the ZoneCodes guide.
What it does
In dense urban areas, a route that looks optimal on paper may cross a bridge, tunnel or toll boundary several times per day, and each crossing costs time, fuel and toll fees. Without explicit modeling, the optimizer treats these transitions as ordinary road segments and produces schedules where drivers zig-zag across zone boundaries unnecessarily.
Zone crossing penalization adds an explicit cost whenever a route transitions from one zone to another. The penalty is configurable per direction: crossing from Zone A to Zone B can carry a different cost than crossing from Zone B to Zone A. The asymmetric directional model captures toll structures, congestion patterns, and one-way crossing costs as multipliers between zones, without encoding them in the distance matrix.
Routes without and with penalization
By default, the optimizer selects routes that minimize total distance and time while respecting constraints. In areas separated by bridges, tunnels or congestion zone boundaries, the locally cheapest route often crosses the boundary several times during a single shift.
Take a truck driver who works in two zones separated by a river bridge:
- Without penalization, the truck crosses the bridge several times throughout the day. Each local routing decision looks efficient, but the crossings add up in tolls, congestion and fuel.
- With penalization, the truck crosses only when necessary, typically once in the morning and once in the evening. The optimizer clusters work within each zone first and crosses only when the global benefit outweighs the penalty.

Without penalization, the routes cross the zone boundary several times.

With penalization, the crossings are reduced to the minimum necessary.
How the penalty is computed
When zone crossing penalization is switched on, the optimizer applies an additional cost to every route edge that transitions from one zone to another. The cost is additive to the normal distance cost:
baseDistanceCost = distance * weight(TotalDistance) * weight(RouteDistance)
totalEdgeCost = baseDistanceCost + (baseDistanceCost * connectionFactor * crossingMultiplier)
connectionFactor is the factor of the ZoneConnection for the direction of the step, 1.0 for a pair without one; crossingMultiplier is the global multiplier. For example, with a multiplier of 3.0 and no ZoneConnection for the pair, a crossing edge has 4 times the distance cost of the same edge without a crossing. The optimizer responds by clustering visits within zones and crossing boundaries only when the overall plan benefits.
The crossing is detected by comparing the ZoneCode qualifications of consecutive nodes in the route. If they belong to different zones, the crossing penalty applies. If they share at least one common zone, no penalty is applied. This works naturally with multi-zone nodes: a border node qualified for both Zone 1 and Zone 2 does not trigger a crossing penalty when visited between a Zone 1 node and a Zone 2 node.
Asymmetric directional penalties
Crossing costs are often directional: a bridge toll may be charged in one direction only, congestion can be heavier inbound in the morning and outbound in the evening, and a controlled border crossing may take longer in one direction. JOpt models this with ZoneConnection objects, each with the multiplier for one direction (see ZoneConnections per direction). A multiplier of 0.0 switches the penalty off for that direction while it stays active for the other.
To our best knowledge, the other routing engines we checked (Google OR-Tools, Timefold, jsprit, VROOM, HERE Tour Planning, PTV) have no built-in zone crossing penalty, and in OR-Tools, jsprit and VROOM a direction-dependent crossing cost can be built into an own asymmetric cost matrix or cost callback. JOpt derives the crossing cost from the ZoneCode qualifications of the nodes, with a global multiplier and ZoneConnections per direction.
When to use it
Use zone crossing penalization when crossing a boundary has a cost that distances and times alone do not show, for example at:
- toll bridges and tunnels;
- congestion zones;
- security checkpoints;
- ports, yards and restricted regions;
- cities split by a river, where switching sides often is inefficient.
The penalty discourages crossings without forbidding them. To forbid zones for a shift, use hard constraints from the ZoneCodes guide.
Zone crossing penalization and territory enforcement
Zone crossing penalization and territory enforcement through ZoneCodes are independent features that work together. A common production pattern combines both:
- Territory enforcement (hard ZoneCodes on WorkingHours): Resource A can only visit Zone 1 and Zone 2 on this shift. Resource B can only visit Zone 3.
- Crossing penalization: Within Resource A's allowed zones (1 and 2), switching between them during a single route is penalized. The resource should cluster work in Zone 1 first, then transition to Zone 2 once, rather than alternating.
This gives two layers of control:
- Feasibility (which zones are allowed) through territory enforcement.
- Efficiency (how often to cross) through crossing penalization.
How to use it
Switch on the penalty
Two properties switch the penalty on and set the global multiplier. The values in the snippet are samples:
Properties props = new Properties();
// Enable zone crossing penalization
props.setProperty("JOpt.Clustering.PenalizeZoneCodeCrossing", "true");
// Set the global default multiplier
props.setProperty("JOpt.Clustering.PenalizeZoneCodeCrossingMultiplier", "10.0");
opti.addElement(props);
The global multiplier applies to every zone transition. A ZoneConnection for a particular pair adds a factor for that direction: the extra cost of a crossing step is its distance times the JOptWeight.TotalDistance and JOptWeight.RouteDistance weights, times the factor of its ZoneConnection, times the global multiplier. The cost is added to the normal distance cost.
ZoneConnections per direction
A ZoneConnection sets the factor for one direction between two zones (crossingPenaltyMultiplier in its builder):
// Crossing from Zone 1 to Zone 3: factor 5.0 (expensive)
ZoneConnection connection13 = ZoneConnection.builder()
.fromZoneId("1")
.toZoneId("3")
.crossingPenaltyMultiplier(5.0)
.build();
// Crossing from Zone 3 to Zone 1: factor 0.0 (penalty off)
ZoneConnection connection31 = ZoneConnection.builder()
.fromZoneId("3")
.toZoneId("1")
.crossingPenaltyMultiplier(0.0)
.build();
In this example, a step from Manhattan (Zone 1) to Jersey City (Zone 3) costs five times the crossing penalty. A step back from Jersey City to Manhattan has the factor 0.0, which switches the penalty off for that direction. This models a toll bridge that charges only in one direction.
The ZoneManager is the registry for zone-to-zone transition rules and holds all ZoneConnection objects. You get it from the node connector of the optimization with opti.getNodeConnector().getZoneManager() and add each connection with putZoneConnection. The first statement of the snippet below shortens this to opti.getZoneManager(), which IOptimization does not have; use the node connector call:
// Get the ZoneManager from the optimization
IZoneManager zm = opti.getZoneManager();
// Define directional connections
ZoneConnection conn13 = ZoneConnection.builder()
.fromZoneId("1").toZoneId("3")
.crossingPenaltyMultiplier(5.0).build();
ZoneConnection conn31 = ZoneConnection.builder()
.fromZoneId("3").toZoneId("1")
.crossingPenaltyMultiplier(0.0).build();
zm.putZoneConnection(conn13);
zm.putZoneConnection(conn31);
You only need to define ZoneConnections for pairs where the penalty differs from the global default. Undeclared pairs have the factor 1.0, so only the global multiplier applies to them.
A city with 10 zones might have 3 toll bridges (6 directional connections) and leave all other transitions at the global default. Only the 6 exceptions need explicit ZoneConnections.
Example: Manhattan and Jersey City
BridgeTunnelCrossingZoneNumberConstraintExample.java models two macro areas separated by the Hudson River. Manhattan nodes are qualified with Zone 1 (and Zone 2 for testing multi-zone). Jersey City nodes are qualified with Zone 3 (and Zone 4). Two directional ZoneConnections define asymmetric toll costs.
The flag doPenalizeZoneCrossings switches the penalty on or off, so you can compare the routes of both runs. The example exports each result to a KML file, BridgeTunnelCrossingZoneNumberConstraintExample-true.kml or BridgeTunnelCrossingZoneNumberConstraintExample-false.kml, for a KML viewer such as Google Earth. With penalization, the routes cross the bridge only when the global benefit outweighs the penalty: in the two test runs on the example page, no step crosses between the zones; one resource visits the eight Jersey City nodes and the other the eight Manhattan nodes, 67 km in total. In an earlier test run without penalization, both routes mixed Manhattan and Jersey City nodes and each crossed the river twice, 65 km in total; the penalty made the plan 2 km longer and removed the crossings.
The zone setup of the nodes:
// Manhattan zones
ZoneNumber zoneOne = new ZoneNumber(1);
ZoneNumber zoneTwo = new ZoneNumber(2);
ZoneNumberQualification manhattanQuali = new ZoneNumberQualification(zoneOne);
manhattanQuali.addExtraCode(zoneTwo);
// Jersey City zones
ZoneNumber zoneThree = new ZoneNumber(3);
ZoneNumber zoneFour = new ZoneNumber(4);
ZoneNumberQualification jerseyQuali = new ZoneNumberQualification(zoneThree);
jerseyQuali.addExtraCode(zoneFour);
The resources in this example have no zone constraints, so they can visit all zones. The penalty works through crossing costs alone, which discourages crossings without prohibiting them.
JSON input for the REST service
In the JSON input of the REST service, the optimization carries its ZoneConnections in the list zoneConnections. Each entry has fromZoneId, toZoneId and crossingPenaltyMultiplier. The two properties are set like any other optimization property. The API reference shows the schema.
Properties
| Property key | What it sets |
|---|---|
JOpt.Clustering.PenalizeZoneCodeCrossing | Switches zone crossing penalization on or off |
JOpt.Clustering.PenalizeZoneCodeCrossingMultiplier | The global multiplier of the crossing penalty |
The defaults and the allowed values are on Optimization properties.
Limits
The penalty is a cost: a route still crosses a boundary when that makes the whole plan cheaper. To keep a resource out of a zone during a shift, use a hard ZoneCode constraint (ZoneCodes and territory management).
Related examples
- Bridge and tunnel crossing example: the example explained, with how to compare both runs and modeling patterns
- BridgeTunnelCrossingZoneNumberConstraintExample.java: the source code on GitHub
Related pages
- ZoneCodes and territory management: zones on WorkingHours and nodes, with hard or soft territories per shift
- Optimization properties: the property keys by category, with their defaults
Authors
DNA Evolutions GmbH
Added: 2026-01-03 (JOpt core 7.5.2-j17) | Last updated: 2026-10-23
ZoneCodes and territories
How ZoneCodes on WorkingHours and on nodes define which resources may visit which nodes per shift, as hard or soft constraints.
Special features
Features of JOpt.TourOptimizer beyond time windows and skills, from CapturedNodes and hard constraints to the Open Assessor and clustering construction, with figures and links to the examples.