Territory Planning (Territory Balancing + Location Allocation)
Builds CARTO Workflows that solve two related spatial optimization problems: dividing areas into balanced territories (e.g. sales regions, service zones) and finding optimal facility locations that maximize coverage or minimize cost.
Prerequisites: Load carto-create-workflow for the development process, JSON structure, and validation. It covers both access paths — the MCP server's workflow tools (create_workflow, validate_workflow, run_workflow) when attached, the carto workflows CLI otherwise. Routing signals: carto-basics/references/access-paths.md. Both components require the Territory Planning Extension Package installed on the connection.
Territory Balancing
Divides a study area into spatially contiguous territories that are balanced by a demand variable (e.g. workload, population, revenue).
Pipeline
Source Data (points) -> Spatial Indexing -> Aggregate per Cell -> Territory Balancing -> Save
Step 1: Load Source Data
Use native.gettablebyname. Input is typically point data with one or more numeric metrics to balance.
Success: Node outputs a table with a geometry column and numeric metric columns.
Step 2: Convert to Spatial Index
Use native.h3frompoint (or native.quadbinfromgeopoint) to assign each point to a grid cell.
Resolution guidance: Higher resolution = smaller cells = finer-grained territories, but more computation. H3 resolution 8-9 is typical for city-level analysis.
Success: Output has a spatial index column (e.g. h3).
Step 3: Aggregate per Cell
Use native.groupby to produce one row per cell with the metrics to balance:
- Group by: the spatial index column (
h3) - Aggregation: e.g.
geom,count,revenue,sum,population,avg
Success: One row per cell with numeric columns for demand and optional similarity features.
Step 4: Run Territory Balancing
Use native.territorybalancing with:
| Input | Description | Example |
|---|---|---|
input_table |
Table input (from previous step) | |
index_column |
H3 or Quadbin column | h3 |
demand_column |
Numeric column to balance across territories | geom_count |
similarity_feats |
Optional numeric columns for within-territory similarity | ["sentiment_avg", "popularity_avg"] |
npartitions |
Number of territories to create | 9 |
keep_input_columns |
Include all input columns in output | true |
Success: Output contains all input rows with an additional territory assignment column. Each cell is assigned to exactly one territory.
Step 5: Save
Use native.saveastable to persist results. The spatial index column is directly visualizable in CARTO Builder, colored by territory ID.
Success: Validated workflow uploadable via create_workflow (MCP) or carto workflows create (CLI).
Location Allocation
Finds the optimal subset of candidate facility locations that either maximize demand coverage or minimize total cost/distance.
Pipeline
Demand Data (grid/points) -> Candidate Locations -> Location Allocation -> Save
Step 1: Load Demand Data
Use native.gettablebyname to load a grid or point dataset representing demand (e.g. population per H3 cell, customer locations).
Success: Table with a spatial index column and a numeric demand column.
Step 2: Load Candidate Locations
Use a second native.gettablebyname for potential facility locations (e.g. existing infrastructure, candidate sites).
If candidates need filtering, use native.wheresimplified or native.limit to reduce the set -- fewer candidates = faster computation.
Success: Table with a spatial index column representing candidate sites.
Step 3: Run Location Allocation
Choose the variant based on the objective:
Maximize Coverage
Use native.locallocallocation_maximizecoverage:
| Input | Description | Example |
|---|---|---|
demand |
Table with demand values | |
demand_index_column |
Spatial index column in demand table | h3 |
demand_column |
Numeric demand variable | population |
candidates |
Table with candidate locations | |
candidates_index_column |
Spatial index column in candidates table | h3 |
nfacilities |
Number of facilities to open | 5 |
coverageradius |
Maximum service distance (meters for geography) | 5000 |
Success: Output identifies which facilities to open and which demand cells each facility serves, maximizing total covered demand within the radius.
Minimize Total Cost
Use native.locallocallocation_minimizetotalcost. Same inputs as Maximize Coverage but without coverageradius — the objective is total weighted travel distance, not coverage within a radius.
Success: Output identifies which facilities to open, minimizing total weighted travel distance between demand and assigned facilities.
Step 4: Save
Use native.saveastable to persist results.
Success: Validated workflow uploadable via create_workflow (MCP) or carto workflows create (CLI).
Gotchas
- Provider casing & SQL dialect. This skill uses lowercase column names (
h3,geom_count,population,sentiment_avg, etc.) — BigQuery / Databricks / Postgres / Redshift convention. On Snowflake, unquoted identifiers surface UPPERCASE — reference them asH3,GEOM_COUNT,POPULATION,SENTIMENT_AVG. Seecarto-create-workflow/references/providers/<provider>.mdfor casing rules and SQL dialect equivalents. - Extension package required: Both territory balancing and location allocation require the Territory Planning Extension Package installed on the specific connection being used. This is NOT available by default -- the user must have it installed. Validation will fail if the package is missing.
- Location allocation is computationally expensive with many candidates. Pre-filter candidates (by geography, capacity, or other criteria) to keep the candidate set small. Hundreds of candidates can be slow; thousands may time out.
- Coverage radius units depend on the spatial reference system -- typically meters when using geography types. Verify the unit matches your intent.
- Territory balancing optimizes a trade-off between spatial contiguity and balance. Perfect balance is not guaranteed -- territories will be approximately equal in the demand variable, not exactly equal.
- Grid resolution sensitivity: Results change significantly with the input grid resolution. Finer grids give more precise boundaries but increase computation. Start with H3 resolution 8 for city-scale and adjust.
- The component names use
native.territorybalancing(no dot separator in "territory balancing") andnative.locallocallocation_maximizecoverage/native.locallocallocation_minimizetotalcost(note the double "local" prefix). - Demand data must cover the study area. Gaps in the demand grid mean those areas are invisible to the optimizer. Use polyfill + COALESCE(value, 0) if you need full coverage.
Reference Templates
Academy Tutorials
- Territory Balancing tutorial -- step-by-step walkthrough of the Milan POS use case
- Location Allocation tutorial -- telco network management with location allocation
- AI Agent + Location Allocation -- using AI agents with location allocation for emergency response hub placement
Common Variations
| Variant | How |
|---|---|
| Sales territory balancing | Points -> H3 -> aggregate revenue per cell -> territory balancing with revenue as demand |
| Service zone design | Grid with population -> territory balancing to create equal-population zones |
| Warehouse/depot placement | Population grid as demand + existing warehouses as candidates -> minimize total cost |
| Emergency response hubs | Population grid as demand + candidate sites -> maximize coverage with a response-time radius |
| Retail network expansion | Customer density grid as demand + potential store locations -> maximize coverage |
| Combined territory + allocation | First run location allocation to pick hub locations, then run territory balancing to assign areas to each hub |