The original routing model was 1,500 lines of C#. The Python port ended up around 480 lines. Some of that compression is the language — Python is more concise. Some of it is that I stripped out the proprietary cloud backend, the database calls, and the dispatch interface. What remained was the core: the model structure, the constraints, the solver parameters.
The translation itself was mostly mechanical. OR-Tools has Python bindings that mirror the C# API closely enough that you’re often just changing syntax: camelCase to snake_case, semicolons disappear, type declarations disappear. But “mostly mechanical” left room for a few things that didn’t work the first time.

The API differences that mattered
The first real snag was the constraint syntax. In C#, OR-Tools lets you write things like solver.MakeEquality(var1, var2) to force two variables to be equal. The Python bindings don’t have that method. Instead you use Python’s native operators directly: solver.Add(var1 == var2). The Python version is actually cleaner — it reads like math — but it’s not obvious if you’re translating line by line from C#.
The same issue came up with the local search operators. The C# code used PascalCase: UseCrossExchange, UseExtendedSwapActive, UseRelocateNeighbors. Python uses snake_case: use_cross_exchange, use_extended_swap_active, use_relocate_neighbors. Not a hard fix, but the kind of thing that produces silent failures — the field name doesn’t match, the option is quietly ignored, the solver runs with default settings instead of the tuned ones, and you have no idea.
I found both of these by checking the actual Python OR-Tools protobuf descriptor, not by guessing. When something doesn’t work and you can’t tell why, look at what the API actually accepts.
The OSRM problem (that wasn’t really a problem)
During development I was running the code in a sandboxed Linux environment that blocked outbound HTTP connections. Which meant the OSRM call — the one that fetches real drive times — returned a 403. The model couldn’t build its distance matrix.
The fix was to add a time_matrix_override parameter to the solver function. If you pass in a matrix, it uses that instead of calling OSRM. For testing I generated distances using the Haversine formula — straight-line approximations converted to drive-time estimates. Completely wrong for real routing, but good enough to verify the model structure worked.
This turned out to be a useful architectural decision beyond just the testing problem. Anyone integrating this model into a system with their own distance data can pass in whatever matrix they have. The solver doesn’t care where the numbers came from.
The parameter question
The original C# code had solver parameters that were tuned over months of production use. Which local search operators to enable. How long to run. Which first-solution heuristic to start with. These choices directly affect solution quality — a poorly configured run can produce routes 20–30% longer than a well-configured one on the same instance.
I kept those parameters exactly as they were in production. Not because I think they’re globally optimal — I don’t know that — but because they represent real calibration work on real data, and that’s worth preserving. A user who uploads their own data might get different results than the original system would have, but at least the solver is starting from a reasonable configuration rather than OR-Tools’ defaults.
The parameters are also exposed in the input workbook, so a technically inclined user can adjust them. Route time limit, search time limit, penalty weights for early and late arrivals — all editable without touching the code.
The warm start
One feature from the original system that made it into the web app: if you provide a previous solution, the solver uses it as a starting point instead of building from scratch. In a real dispatch operation, today’s routes are often similar to yesterday’s — same stops, slightly different windows. Starting from a known-good solution and improving it is faster than starting from zero.
The web app reads the previous solution from the same workbook’s solution sheet. Leave it empty, and the solver does a fresh run. Fill it in with a prior result, and it warm-starts. Simple, and it preserves one of the more practically useful features of the original system.
What the test run looked like
After the port, I ran the engine against a small test dataset — 10 stops, 3 trucks, haversine distances, 10-second time limit. Status: OK. 1 route. 10 stops assigned. 0 dropped. The model ran OK…
Then I ran the full 113-stop sample against OSRM on Render and got back actual routes with real drive times. That’s when it felt done.
Next: wrapping it in Flask and deploying it — which was, as usual, easier than the model and harder than it looked.
Things that I use, like, and am affiliated with:
Mint Mobile offers great cell phone service for $15 flat, get $15 off using the link. Get discounted phones with service activation and no contract.
I never spend money before I check Mr Rebates or Rakuten to get cashbacks, rebates, discounts, coupons or cheaper gift cards.
