Home | Benchmarks | Categories | Atom Feed

Posted on Wed 25 February 2026 under Solvers

Data Crew's Route Optimiser Framework

Data Crew is a consultancy that was started last year in Argentina. They've recently released a 1,419-line, Python-based Route Optimiser Framework.

It supports both the Chinese Postman (CPP) and Travelling Salesman (TSP) problems. The framework is meant to be easy to expand and there are plans to add support for Vehicle Routing (VRP), Capacitated VRP (CVRP) and VRP with Time Windows (VRPTW) at a later date.

In this post, I'll examine two example solvers from Data Crew's Route Optimiser Framework.

My Workstation

I'm using a 5.7 GHz AMD Ryzen 9 9950X CPU. It has 16 cores and 32 threads and 1.2 MB of L1, 16 MB of L2 and 64 MB of L3 cache. It has a liquid cooler attached and is housed in a spacious, full-sized Cooler Master HAF 700 computer case.

The system has 96 GB of DDR5 RAM clocked at 4,800 MT/s and a 5th-generation, Crucial T700 4 TB NVMe M.2 SSD which can read at speeds up to 12,400 MB/s. There is a heatsink on the SSD to help keep its temperature down. This is my system's C drive.

The system is powered by a 1,200-watt, fully modular Corsair Power Supply and is sat on an ASRock X870E Nova 90 Motherboard.

I'm running Ubuntu 24 LTS via Microsoft's Ubuntu for Windows on Windows 11 Pro. In case you're wondering why I don't run a Linux-based desktop as my primary work environment, I'm still using an Nvidia GTX 1080 GPU which has better driver support on Windows and ArcGIS Pro only supports Windows natively.

Installing Prerequisites

I'll use Python 3.12.3 and jq to help analyse the data in this post.

$ sudo add-apt-repository ppa:deadsnakes/ppa
$ sudo apt update
$ sudo apt install \
    jq \
    python3-pip \
    python3.12-venv

Below, I'll set up a Python Virtual Environment and install some dependencies.

$ python3 -m venv ~/.route-optimizer
$ source ~/.route-optimizer/bin/activate
$ python3 -m pip install \
    contextily \
    folium \
    geopandas \
    matplotlib \
    networkx \
    openpyxl \
    osmnx \
    pandas \
    scikit-learn \
    shapely

I'll clone the repo that contains the framework as well as some example scenarios that I'll walk through below.

$ git clone https://github.com/Data-Crew/route-optimizer \
    ~/route-optimizer

I'll use DuckDB v1.4.3, along with its H3, JSON, Lindel, Parquet and Spatial extensions, in this post.

$ cd ~
$ wget -c https://github.com/duckdb/duckdb/releases/download/v1.4.3/duckdb_cli-linux-amd64.zip
$ unzip -j duckdb_cli-linux-amd64.zip
$ chmod +x duckdb
$ ~/duckdb
INSTALL h3 FROM community;
INSTALL lindel FROM community;
INSTALL json;
INSTALL parquet;
INSTALL spatial;

I'll set up DuckDB to load every installed extension each time it launches.

$ vi ~/.duckdbrc
.timer on
.width 180
LOAD h3;
LOAD lindel;
LOAD json;
LOAD parquet;
LOAD spatial;

Parking Enforcement

The first example is a parking enforcement solver. An inspector needs the shortest path along a circuit that visits every edge at least once.

Below is the configuration file that this solver will call upon. It isolates a bounding box around La Plata, Argentina, a planned city with diagonal avenues, which is to the southeast of Buenos Aires.

This bounding box will be used to isolate the OpenStreetMap (OSM) road network for this area via the OSMnx library.

There are also three zones defined within this area, each with its own unique parking restrictions.

$ cd ~/route-optimizer
$ cat zone_config.json
{
  "configuration": {
    "city": "La Plata, Argentina",
    "bbox": {
      "description": "Bounding box (west, south, east, north)",
      "west": -57.9605,
      "south": -34.9210,
      "east": -57.9455,
      "north": -34.9095
    },
    "start_point": {
      "description": "Start and return point (lat, lon)",
      "name": "Corner of 12th and 50th",
      "latitude": -34.91719,
      "longitude": -57.95067
    }
  },
  "zones": [
    {
      "name": "courthouse",
      "description": "Courthouse Zone",
      "polygon": [
        [-57.957, -34.915],
        [-57.949, -34.915],
        [-57.949, -34.911],
        [-57.957, -34.911]
      ],
      "schedule": {
        "start": "08:00",
        "end": "14:00"
      },
      "weekdays": [0, 1, 2, 3, 4],
      "weekday_names": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
      "prohibited_streets": [],
      "color": "pink"
    },
    {
      "name": "downtown",
      "description": "Downtown Zone",
      "polygon": [
        [-57.955, -34.9145],
        [-57.9475, -34.9145],
        [-57.9475, -34.9185],
        [-57.955, -34.9185]
      ],
      "schedule": {
        "start": "08:00",
        "end": "20:00"
      },
      "weekdays": [0, 1, 2, 3, 4, 5],
      "weekday_names": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
      "prohibited_streets": [],
      "color": "gold"
    },
    {
      "name": "12th_street",
      "description": "12th Street Zone",
      "polygon": [
        [-57.955, -34.9125],
        [-57.952, -34.9125],
        [-57.952, -34.9195],
        [-57.955, -34.9195]
      ],
      "schedule": {
        "start": "08:00",
        "end": "20:00"
      },
      "weekdays": [0, 1, 2, 3, 4, 5],
      "weekday_names": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
      "prohibited_streets": [],
      "color": "violet"
    }
  ],
  "route_types": {
    "full": {
      "name": "Full Route",
      "description": "All zones (Monday to Friday 8am-8pm)",
      "zones": ["courthouse", "downtown", "12th_street"]
    },
    "no_courthouse": {
      "name": "No Courthouse",
      "description": "For enforcement officers working after 2pm",
      "zones": ["downtown", "12th_street"]
    },
    "saturday": {
      "name": "Saturday",
      "description": "Weekend route",
      "zones": ["downtown", "12th_street"]
    }
  }
}

Below, I'll run the solver. This first deliverable is a map of the zone within the bounding box.

$ python3 parking_enforcement.py
======================================================================
 ROUTE OPTIMIZER - PARKING ENFORCEMENT (CPP)
======================================================================
πŸ“₯ Downloading street network...
βœ… Network downloaded: 208 nodes, 369 edges
πŸ“ Start node: 249417335

🏷️  Labeling zones...
   β€’ courthouse: 39 nodes
   β€’ downtown: 34 nodes
   β€’ 12th_street: 27 nodes

πŸ“Š Zone summary:
zone
outside        135
courthouse      29
12th_street     27
downtown        17
Name: count, dtype: int64

πŸ—ΊοΈ  Generating visualization...
πŸ’Ύ Map saved to: output/cpp_zones.png

Then, the full route is calculated and results in a solution that is 11.06 KM long.

======================================================================
 CALCULATING OPTIMAL ROUTES
======================================================================

πŸ“ CASE 1: Full Route

============================================================
πŸš€ SOLVING ROUTE: FULL (CPP)
============================================================

πŸ” Route type 'full':
   β€’ Zones: courthouse, downtown, 12th_street
   β€’ Nodes: 73
   β€’ Edges: 123

πŸ”„ Solving Chinese Postman Problem (CPP)...
   ⚠️  Graph not strongly connected. Extracting main component...
   πŸ”„ Eulerizing graph...
      β€’ 13 unbalanced nodes
      βœ… Eulerized: 139 edges
   πŸ” Finding Eulerian circuit...
   ⚠️  Could not create perfect Eulerian circuit
   πŸ“ Using approximation...
      πŸ“ Expanded 11 path segments with intermediate nodes
   βœ… Route found: 119 nodes, 11.09 km

βœ… Route calculated (CPP):
   β€’ Nodes visited: 119
   β€’ Total distance: 11.09 km

The next calculation handles the "no courthouse" custom restrictions and produces a solution with a distance of 5.39 KM.

πŸ“ CASE 2: No Courthouse (after 2pm)

============================================================
πŸš€ SOLVING ROUTE: NO_COURTHOUSE (CPP)
============================================================

πŸ” Route type 'no_courthouse':
   β€’ Zones: downtown, 12th_street
   β€’ Nodes: 44
   β€’ Edges: 69

πŸ”„ Solving Chinese Postman Problem (CPP)...
   ⚠️  Graph not strongly connected. Extracting main component...
   πŸ”„ Eulerizing graph...
      β€’ 10 unbalanced nodes
      βœ… Eulerized: 79 edges
   πŸ” Finding Eulerian circuit...
   ⚠️  Could not create perfect Eulerian circuit
   πŸ“ Using approximation...
      πŸ“ Expanded 4 path segments with intermediate nodes
   βœ… Route found: 59 nodes, 5.39 km

βœ… Route calculated (CPP):
   β€’ Nodes visited: 59
   β€’ Total distance: 5.39 km

The next calculation handles the Saturday route and produces a solution with a distance of 5.39 KM.

πŸ“ CASE 3: Saturday Route

============================================================
πŸš€ SOLVING ROUTE: SATURDAY (CPP)
============================================================

πŸ” Route type 'saturday':
   β€’ Zones: downtown, 12th_street
   β€’ Nodes: 44
   β€’ Edges: 69

πŸ”„ Solving Chinese Postman Problem (CPP)...
   ⚠️  Graph not strongly connected. Extracting main component...
   πŸ”„ Eulerizing graph...
      β€’ 10 unbalanced nodes
      βœ… Eulerized: 79 edges
   πŸ” Finding Eulerian circuit...
   ⚠️  Could not create perfect Eulerian circuit
   πŸ“ Using approximation...
      πŸ“ Expanded 4 path segments with intermediate nodes
   βœ… Route found: 59 nodes, 5.39 km

βœ… Route calculated (CPP):
   β€’ Nodes visited: 59
   β€’ Total distance: 5.39 km

The results of the three above cases are then saved to an Excel file.

πŸ“Š Exporting results...
βœ… Results exported to: output/cpp_routes.xlsx

πŸ“‹ Summary:
   Route_Type  Node_Count  Distance_KM  Distance_Meters
         full         119        11.09            11093
no_courthouse          59         5.39             5392
     saturday          59         5.39             5392

Below is the summary sheet from that file.

$ echo "FROM ST_READ('output/cpp_routes.xlsx')" \
    | ~/duckdb -json \
    | jq -S .
[
  {
    "Distance_KM": 11.09,
    "Distance_Meters": 11093,
    "Node_Count": 119,
    "Route_Type": "full"
  },
  {
    "Distance_KM": 5.39,
    "Distance_Meters": 5392,
    "Node_Count": 59,
    "Route_Type": "no_courthouse"
  },
  {
    "Distance_KM": 5.39,
    "Distance_Meters": 5392,
    "Node_Count": 59,
    "Route_Type": "saturday"
  }
]

Each case has its own sheet with its sequence list. Below is the sheet for the Saturday route.

$ ~/duckdb
.maxrows 60

SELECT sequence: Sequence::INT,
       node_id:  Node_ID::BIGINT,
       geom:     ST_POINT(Longitude, Latitude),
       zone:     Zone
FROM   READ_XLSX('output/cpp_routes.xlsx',
                 sheet='saturday');
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ sequence β”‚  node_id   β”‚              geom               β”‚    zone     β”‚
β”‚  int32   β”‚   int64    β”‚            geometry             β”‚   varchar   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚        1 β”‚  249417335 β”‚ POINT (-57.9510606 -34.91725)   β”‚ downtown    β”‚
β”‚        2 β”‚  249417330 β”‚ POINT (-57.9517689 -34.9167244) β”‚ downtown    β”‚
β”‚        3 β”‚  249417040 β”‚ POINT (-57.9527766 -34.9176466) β”‚ 12th_street β”‚
β”‚        4 β”‚  249416478 β”‚ POINT (-57.9538079 -34.9185892) β”‚ 12th_street β”‚
β”‚        5 β”‚ 6127585995 β”‚ POINT (-57.9546872 -34.9193893) β”‚ 12th_street β”‚
β”‚        6 β”‚ 6127586000 β”‚ POINT (-57.9547393 -34.918265)  β”‚ 12th_street β”‚
β”‚        7 β”‚  249416467 β”‚ POINT (-57.9545168 -34.9180625) β”‚ 12th_street β”‚
β”‚        8 β”‚  249416459 β”‚ POINT (-57.9547554 -34.9178853) β”‚ 12th_street β”‚
β”‚        9 β”‚  249418061 β”‚ POINT (-57.954798 -34.9169539)  β”‚ 12th_street β”‚
β”‚       10 β”‚  249417004 β”‚ POINT (-57.9548565 -34.9161046) β”‚ 12th_street β”‚
β”‚       11 β”‚  249418057 β”‚ POINT (-57.9548928 -34.9155776) β”‚ 12th_street β”‚
β”‚       12 β”‚  249417305 β”‚ POINT (-57.9541766 -34.9149376) β”‚ 12th_street β”‚
β”‚       13 β”‚  249417664 β”‚ POINT (-57.9531609 -34.9140082) β”‚ 12th_street β”‚
β”‚       14 β”‚  249417665 β”‚ POINT (-57.9522478 -34.9146926) β”‚ 12th_street β”‚
β”‚       15 β”‚  249417312 β”‚ POINT (-57.9532914 -34.9155946) β”‚ 12th_street β”‚
β”‚       16 β”‚  249417026 β”‚ POINT (-57.9543003 -34.9165164) β”‚ 12th_street β”‚
β”‚       17 β”‚  249417033 β”‚ POINT (-57.9534809 -34.9171231) β”‚ 12th_street β”‚
β”‚       18 β”‚  249417320 β”‚ POINT (-57.9524713 -34.9162031) β”‚ 12th_street β”‚
β”‚       19 β”‚  249417672 β”‚ POINT (-57.9514609 -34.9152824) β”‚ downtown    β”‚
β”‚       20 β”‚  249417675 β”‚ POINT (-57.9507639 -34.9158047) β”‚ downtown    β”‚
β”‚       21 β”‚  249417679 β”‚ POINT (-57.9500587 -34.9163333) β”‚ downtown    β”‚
β”‚       22 β”‚ 1868768411 β”‚ POINT (-57.948899 -34.9171887)  β”‚ downtown    β”‚
β”‚       23 β”‚  249417681 β”‚ POINT (-57.9488464 -34.9172257) β”‚ downtown    β”‚
β”‚       24 β”‚ 2761285280 β”‚ POINT (-57.9478281 -34.9163417) β”‚ downtown    β”‚
β”‚       25 β”‚ 1868768407 β”‚ POINT (-57.9477386 -34.9162624) β”‚ downtown    β”‚
β”‚       26 β”‚ 2714068787 β”‚ POINT (-57.9478392 -34.9161947) β”‚ downtown    β”‚
β”‚       27 β”‚ 2761285279 β”‚ POINT (-57.9479159 -34.9162708) β”‚ downtown    β”‚
β”‚       28 β”‚ 2761285280 β”‚ POINT (-57.9478281 -34.9163417) β”‚ downtown    β”‚
β”‚       29 β”‚ 1868768407 β”‚ POINT (-57.9477386 -34.9162624) β”‚ downtown    β”‚
β”‚       30 β”‚ 2714068787 β”‚ POINT (-57.9478392 -34.9161947) β”‚ downtown    β”‚
β”‚       31 β”‚  249417867 β”‚ POINT (-57.9496921 -34.9147895) β”‚ downtown    β”‚
β”‚       32 β”‚ 2761285275 β”‚ POINT (-57.949781 -34.9148569)  β”‚ downtown    β”‚
β”‚       33 β”‚ 2761285277 β”‚ POINT (-57.9490549 -34.9154107) β”‚ downtown    β”‚
β”‚       34 β”‚ 2761285279 β”‚ POINT (-57.9479159 -34.9162708) β”‚ downtown    β”‚
β”‚       35 β”‚ 1868768411 β”‚ POINT (-57.948899 -34.9171887)  β”‚ downtown    β”‚
β”‚       36 β”‚ 1868768422 β”‚ POINT (-57.9499061 -34.9181067) β”‚ downtown    β”‚
β”‚       37 β”‚  249417335 β”‚ POINT (-57.9510606 -34.91725)   β”‚ downtown    β”‚
β”‚       38 β”‚  249417330 β”‚ POINT (-57.9517689 -34.9167244) β”‚ downtown    β”‚
β”‚       39 β”‚  249417320 β”‚ POINT (-57.9524713 -34.9162031) β”‚ 12th_street β”‚
β”‚       40 β”‚  249417312 β”‚ POINT (-57.9532914 -34.9155946) β”‚ 12th_street β”‚
β”‚       41 β”‚  249417026 β”‚ POINT (-57.9543003 -34.9165164) β”‚ 12th_street β”‚
β”‚       42 β”‚  249418061 β”‚ POINT (-57.954798 -34.9169539)  β”‚ 12th_street β”‚
β”‚       43 β”‚ 6127586001 β”‚ POINT (-57.954951 -34.9170952)  β”‚ 12th_street β”‚
β”‚       44 β”‚ 6127585996 β”‚ POINT (-57.9549152 -34.9177665) β”‚ 12th_street β”‚
β”‚       45 β”‚ 2760842529 β”‚ POINT (-57.9548801 -34.9183932) β”‚ 12th_street β”‚
β”‚       46 β”‚ 6127586000 β”‚ POINT (-57.9547393 -34.918265)  β”‚ 12th_street β”‚
β”‚       47 β”‚  249416467 β”‚ POINT (-57.9545168 -34.9180625) β”‚ 12th_street β”‚
β”‚       48 β”‚  249417033 β”‚ POINT (-57.9534809 -34.9171231) β”‚ 12th_street β”‚
β”‚       49 β”‚  249417040 β”‚ POINT (-57.9527766 -34.9176466) β”‚ 12th_street β”‚
β”‚       50 β”‚  249417046 β”‚ POINT (-57.9520687 -34.9181727) β”‚ 12th_street β”‚
β”‚       51 β”‚  249416487 β”‚ POINT (-57.9530977 -34.9191169) β”‚ 12th_street β”‚
β”‚       52 β”‚  249416478 β”‚ POINT (-57.9538079 -34.9185892) β”‚ 12th_street β”‚
β”‚       53 β”‚  249416467 β”‚ POINT (-57.9545168 -34.9180625) β”‚ 12th_street β”‚
β”‚       54 β”‚  249417033 β”‚ POINT (-57.9534809 -34.9171231) β”‚ 12th_street β”‚
β”‚       55 β”‚  249417320 β”‚ POINT (-57.9524713 -34.9162031) β”‚ 12th_street β”‚
β”‚       56 β”‚  249417672 β”‚ POINT (-57.9514609 -34.9152824) β”‚ downtown    β”‚
β”‚       57 β”‚  249417675 β”‚ POINT (-57.9507639 -34.9158047) β”‚ downtown    β”‚
β”‚       58 β”‚  249417679 β”‚ POINT (-57.9500587 -34.9163333) β”‚ downtown    β”‚
β”‚       59 β”‚  249417335 β”‚ POINT (-57.9510606 -34.91725)   β”‚ downtown    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 59 rows                                                     4 columns β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Images of each case, along with interactive maps, are then generated.

======================================================================
 GENERATING ROUTE VISUALIZATIONS
======================================================================

πŸ” Route type 'full':
   β€’ Zones: courthouse, downtown, 12th_street
   β€’ Nodes: 73
   β€’ Edges: 123

πŸ” Route type 'no_courthouse':
   β€’ Zones: downtown, 12th_street
   β€’ Nodes: 44
   β€’ Edges: 69

πŸ” Route type 'saturday':
   β€’ Zones: downtown, 12th_street
   β€’ Nodes: 44
   β€’ Edges: 69

πŸ“Έ Generating static images...

πŸ—ΊοΈ  Visualizing route: CPP: Full Route (All Zones)
πŸ’Ύ Route saved to: output/cpp_route_full.png

πŸ—ΊοΈ  Visualizing route: CPP: No Courthouse Route (After 2pm)
πŸ’Ύ Route saved to: output/cpp_route_no_courthouse.png

πŸ—ΊοΈ  Visualizing route: CPP: Saturday Route
πŸ’Ύ Route saved to: output/cpp_route_saturday.png

🌐 Generating interactive maps (open in browser)...

πŸ—ΊοΈ  Generating interactive map: CPP: Full Route (All Zones)
πŸ’Ύ Interactive map saved to: output/cpp_route_full.html
   Open in browser and use the playback controls!

πŸ—ΊοΈ  Generating interactive map: CPP: No Courthouse Route (After 2pm)
πŸ’Ύ Interactive map saved to: output/cpp_route_no_courthouse.html
   Open in browser and use the playback controls!

πŸ—ΊοΈ  Generating interactive map: CPP: Saturday Route
πŸ’Ύ Interactive map saved to: output/cpp_route_saturday.html
   Open in browser and use the playback controls!

======================================================================
 FINAL SUMMARY - CPP (Chinese Postman Problem)
======================================================================

βœ… Full Route:      11.09 km (119 nodes)
βœ… No Courthouse:   5.39 km (59 nodes)
βœ… Saturday:        5.39 km (59 nodes)

πŸ“ Output files (all prefixed with 'cpp_'):

   πŸ“Έ Static images (with street basemap):
   β€’ cpp_zones.png              - Map of enforcement zones
   β€’ cpp_route_full.png         - Full route
   β€’ cpp_route_no_courthouse.png - Afternoon route
   β€’ cpp_route_saturday.png     - Weekend route

   🌐 Interactive maps (RECOMMENDED - open in browser):
   β€’ cpp_route_full.html        - Full route with playback
   β€’ cpp_route_no_courthouse.html - Afternoon route
   β€’ cpp_route_saturday.html    - Weekend route

   πŸ“Š Data:
   β€’ cpp_routes.xlsx            - Route coordinates

πŸ’‘ TIP: Open the .html files for interactive step-by-step playback!

======================================================================

Below is a capture of the interactive map of the full route. It has been sped up 9x.

Data Crew's Route Optimiser Framework

Below is the static image generated of the route with no courthouse.

Data Crew's Route Optimiser Framework

Below is the static image generated of the Saturday route.

Data Crew's Route Optimiser Framework

Delivery Routing

Below is the second example solver. It finds the shortest possible distance a delivery driver would need to travel in order to deliver ten packages.

$ python3 delivery_route.py
======================================================================
 ROUTE OPTIMIZER - DELIVERY ROUTE (TSP)
======================================================================

πŸ“¦ Scenario: A delivery driver has 10 packages to deliver.
   Goal: Find the shortest route visiting all addresses.

πŸ“₯ Downloading street network...
βœ… Network downloaded: 208 nodes, 369 edges
πŸ“ Start node: 249417335

🏷️  Labeling zones...
   β€’ delivery_area: 79 nodes

πŸ“Š Zone summary:
zone
outside          129
delivery_area     79
Name: count, dtype: int64

πŸ“ Delivery addresses (10 packages):
   πŸ“¦ Package 1: Node 249417664 (-34.91401, -57.95316)
   πŸ“¦ Package 2: Node 249417672 (-34.91528, -57.95146)
   πŸ“¦ Package 3: Node 249417679 (-34.91633, -57.95006)
   πŸ“¦ Package 4: Node 249417046 (-34.91817, -57.95207)
   πŸ“¦ Package 5: Node 4771931492 (-34.91434, -57.95498)
   πŸ“¦ Package 6: Node 249416993 (-34.91586, -57.95518)
   πŸ“¦ Package 7: Node 249417033 (-34.91712, -57.95348)
   πŸ“¦ Package 8: Node 249417862 (-34.91428, -57.95036)
   πŸ“¦ Package 9: Node 249417312 (-34.91559, -57.95329)
   πŸ“¦ Package 10: Node 249417335 (-34.91725, -57.95106)

   ℹ️  10 addresses β†’ 10 unique stops

======================================================================
 CALCULATING OPTIMAL ROUTE (TSP)
======================================================================

============================================================
πŸš€ SOLVING ROUTE: FULL (TSP)
============================================================

πŸ” Route type 'full':
   β€’ Zones: delivery_area
   β€’ Nodes: 79
   β€’ Edges: 135

πŸ”„ Solving Traveling Salesman Problem (TSP)...
   πŸ“ Nodes to visit: 10
   ⚠️  Graph not strongly connected. Extracting main component...
   πŸ“Š Computing distance matrix...
   πŸ” Finding optimal tour...
      πŸ“ Expanded 8 path segments with intermediate nodes
   βœ… Route found: 10 stops, 37 total nodes, 3.60 km

βœ… Route calculated (TSP):
   β€’ Nodes visited: 37
   β€’ Total distance: 3.60 km

πŸ“Š Route summary:
   β€’ Packages: 10
   β€’ Unique stops: 10
   β€’ Route nodes: 37 (including path between stops)
   β€’ Total distance: 3.60 km

An Excel file of the results, along with images and an interactive map are then generated from the above solution.

======================================================================
 GENERATING VISUALIZATIONS
======================================================================

πŸ—ΊοΈ  Generating visualization...
πŸ’Ύ Map saved to: output/tsp_zone.png

πŸ” Route type 'full':
   β€’ Zones: delivery_area
   β€’ Nodes: 79
   β€’ Edges: 135

πŸ—ΊοΈ  Visualizing route: TSP: Delivery Route (10 packages)
πŸ’Ύ Route saved to: output/tsp_route.png

πŸ—ΊοΈ  Generating interactive map: TSP: Delivery Route
πŸ’Ύ Interactive map saved to: output/tsp_route.html
   Open in browser and use the playback controls!

πŸ“Š Exporting results...
βœ… Results exported to: output/tsp_routes.xlsx

πŸ“‹ Summary:
    Route_Type  Node_Count  Distance_KM  Distance_Meters
delivery_route          37          3.6             3598

======================================================================
 FINAL SUMMARY - TSP (Traveling Salesman Problem)
======================================================================

βœ… Delivery route optimized!
   β€’ Packages: 10
   β€’ Distance: 3.60 km
   β€’ Estimated time: ~7 min (at 30 km/h avg)

πŸ“ Output files (all prefixed with 'tsp_'):

   πŸ“Έ Static images:
   β€’ tsp_zone.png   - Delivery area map
   β€’ tsp_route.png  - Optimized route with basemap

   🌐 Interactive map:
   β€’ tsp_route.html - Route with step-by-step playback

   πŸ“Š Data:
   β€’ tsp_routes.xlsx - Route coordinates
======================================================================

Below is a capture of the interactive map. It has been sped up 9x.

Data Crew's Route Optimiser Framework

Below is the route summary sheet.

$ echo "FROM ST_READ('output/tsp_routes.xlsx')" \
    | ~/duckdb -json \
    | jq -S .
[
  {
    "Distance_KM": 3.6,
    "Distance_Meters": 3598,
    "Node_Count": 37,
    "Route_Type": "delivery_route"
  }
]

Below is the sequence list.

SELECT sequence: Sequence::INT,
       node_id:  Node_ID::BIGINT,
       geom:     ST_POINT(Longitude, Latitude),
       zone:     Zone
FROM   READ_XLSX('output/tsp_routes.xlsx',
                 sheet='delivery_route');
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ sequence β”‚  node_id   β”‚              geom               β”‚     zone      β”‚
β”‚  int32   β”‚   int64    β”‚            geometry             β”‚    varchar    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚        1 β”‚  249417335 β”‚ POINT (-57.9510606 -34.91725)   β”‚ delivery_area β”‚
β”‚        2 β”‚  249417330 β”‚ POINT (-57.9517689 -34.9167244) β”‚ delivery_area β”‚
β”‚        3 β”‚  249417320 β”‚ POINT (-57.9524713 -34.9162031) β”‚ delivery_area β”‚
β”‚        4 β”‚  249417672 β”‚ POINT (-57.9514609 -34.9152824) β”‚ delivery_area β”‚
β”‚        5 β”‚ 2761285274 β”‚ POINT (-57.9504434 -34.9143554) β”‚ delivery_area β”‚
β”‚        6 β”‚  249417862 β”‚ POINT (-57.950361 -34.9142803)  β”‚ delivery_area β”‚
β”‚        7 β”‚  249417857 β”‚ POINT (-57.9511569 -34.913688)  β”‚ delivery_area β”‚
β”‚        8 β”‚  249417851 β”‚ POINT (-57.9520626 -34.9130139) β”‚ delivery_area β”‚
β”‚        9 β”‚  249417849 β”‚ POINT (-57.9529888 -34.9123245) β”‚ delivery_area β”‚
β”‚       10 β”‚ 2761285268 β”‚ POINT (-57.9530811 -34.9123952) β”‚ delivery_area β”‚
β”‚       11 β”‚  249417663 β”‚ POINT (-57.9540895 -34.9133121) β”‚ delivery_area β”‚
β”‚       12 β”‚  249417664 β”‚ POINT (-57.9531609 -34.9140082) β”‚ delivery_area β”‚
β”‚       13 β”‚  249417665 β”‚ POINT (-57.9522478 -34.9146926) β”‚ delivery_area β”‚
β”‚       14 β”‚  249417312 β”‚ POINT (-57.9532914 -34.9155946) β”‚ delivery_area β”‚
β”‚       15 β”‚  249417026 β”‚ POINT (-57.9543003 -34.9165164) β”‚ delivery_area β”‚
β”‚       16 β”‚  249417033 β”‚ POINT (-57.9534809 -34.9171231) β”‚ delivery_area β”‚
β”‚       17 β”‚  249417320 β”‚ POINT (-57.9524713 -34.9162031) β”‚ delivery_area β”‚
β”‚       18 β”‚  249417312 β”‚ POINT (-57.9532914 -34.9155946) β”‚ delivery_area β”‚
β”‚       19 β”‚  249417305 β”‚ POINT (-57.9541766 -34.9149376) β”‚ delivery_area β”‚
β”‚       20 β”‚ 4771931492 β”‚ POINT (-57.9549762 -34.9143443) β”‚ delivery_area β”‚
β”‚       21 β”‚ 6040327689 β”‚ POINT (-57.9551144 -34.9142417) β”‚ delivery_area β”‚
β”‚       22 β”‚ 2760842520 β”‚ POINT (-57.9550254 -34.9157142) β”‚ delivery_area β”‚
β”‚       23 β”‚ 2760842522 β”‚ POINT (-57.9550061 -34.9159896) β”‚ delivery_area β”‚
β”‚       24 β”‚  249417004 β”‚ POINT (-57.9548565 -34.9161046) β”‚ delivery_area β”‚
β”‚       25 β”‚  249417026 β”‚ POINT (-57.9543003 -34.9165164) β”‚ delivery_area β”‚
β”‚       26 β”‚  249417033 β”‚ POINT (-57.9534809 -34.9171231) β”‚ delivery_area β”‚
β”‚       27 β”‚  249417040 β”‚ POINT (-57.9527766 -34.9176466) β”‚ delivery_area β”‚
β”‚       28 β”‚  249417046 β”‚ POINT (-57.9520687 -34.9181727) β”‚ delivery_area β”‚
β”‚       29 β”‚  249416487 β”‚ POINT (-57.9530977 -34.9191169) β”‚ delivery_area β”‚
β”‚       30 β”‚  249416478 β”‚ POINT (-57.9538079 -34.9185892) β”‚ delivery_area β”‚
β”‚       31 β”‚  249416467 β”‚ POINT (-57.9545168 -34.9180625) β”‚ delivery_area β”‚
β”‚       32 β”‚  249417033 β”‚ POINT (-57.9534809 -34.9171231) β”‚ delivery_area β”‚
β”‚       33 β”‚  249417320 β”‚ POINT (-57.9524713 -34.9162031) β”‚ delivery_area β”‚
β”‚       34 β”‚  249417672 β”‚ POINT (-57.9514609 -34.9152824) β”‚ delivery_area β”‚
β”‚       35 β”‚  249417675 β”‚ POINT (-57.9507639 -34.9158047) β”‚ delivery_area β”‚
β”‚       36 β”‚  249417679 β”‚ POINT (-57.9500587 -34.9163333) β”‚ delivery_area β”‚
β”‚       37 β”‚  249417335 β”‚ POINT (-57.9510606 -34.91725)   β”‚ delivery_area β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 37 rows                                                       4 columns β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Thank you for taking the time to read this post. I offer both consulting and hands-on development services to clients in North America and Europe. If you'd like to discuss how my offerings can help your business please contact me via LinkedIn.

Copyright © 2014 - 2026 Mark Litwintschik. This site's template is based off a template by Giulio Fidente.