Overview
We split Calgary into equal hexagons and, for each one, compare what its properties pay in municipal property tax with what it costs to look after its roads. The roads are measured against mobility's share of that tax, 6.62 cents of every dollar, because that is the part of the tax bill meant for streets, sidewalks, lights and signals.
The method has four steps:
- Grid. Cover the city in 800 m hexagons and tag each with its community, era and ward.
- Revenue. Spread every property's assessed value into the hexagons it sits in, then apply the 2026 municipal tax rates.
- Roads. Measure the length of each class of road in each hexagon and turn it into square metres of pavement.
- Cost. Price that pavement under each pavement plan and compare it with the hexagon's mobility share.
All figures are for the 2026 tax year unless noted. The 2025 roll was used to check the method against the City's published totals.
1. The hex grid
Each hexagon is 800 m across, about a ten-minute walk, and covers 55.4 hectares (0.55 km²). Equal-sized cells let us compare places fairly; community boundaries vary too much in size to do that.
| Measure | Value |
|---|---|
| Width, flat side to flat side | 800 m |
| Width, corner to corner | 924 m |
| Side length | 462 m |
| Area of a full hexagon | 55.4 ha |
| Hexagons in the grid | 1,683 (1,411 full, 272 cut by the city limit) |
| Total area covered | 853 km² |
| Ordinary neighbourhood hexagons used in the counts | 1,236 |
The grid was built in QGIS and clipped to the city boundary, so hexagons along the edge are smaller. It is stored in NAD83 / UTM zone 12N (EPSG:26912), so all lengths and areas are in true metres.
Tags. Each hexagon takes the community, community structure (era, such as "1960s/1970s" or "Inner city") and ward that its centre point falls in. Three edge hexagons whose centres fall outside every community take the nearest one instead.
Special areas. Hexagons tagged Parks, Undeveloped or Other (special districts such as the airport) stay in every citywide total but are greyed out on the map and left out of the neighbourhood counts. There are 435 of them. They are not expected to pay for their own roads.
2. Revenue: what each hexagon pays
Each property's assessed value is shared among the hexagons it overlaps, in proportion to how much of its land falls in each, and then taxed at the City's 2026 municipal rates. In 2026 the model raises $2,548M in municipal property tax from $421B of taxable assessed value.
Step 1: one value per parcel. The 2026 assessment roll lists each account (roll number) with its land parcel. Where one roll covers several parcels, the roll repeats its full value on each, so we split it by land area:
Without this step, about $25B of value was counted twice.
Step 2: from parcels to hexagons. A parcel that straddles a hexagon edge is split by area:
The denominator is the parcel's area inside the whole grid, so every dollar of a parcel's value lands in some hexagon. This is done separately for residential, non-residential and farm value.
Step 3: tax. Revenue is the value of each class times its 2026 municipal rate:
The provincial education tax (0.0027593 residential and farm, 0.0041502 non-residential) is calculated too, but only to check totals. It is never counted as money available for roads.
Exempt property. Property in two groups of the roll's sub-type codes pays no tax in the model: institutional (codes starting with S: hospitals, schools, universities) and recreation (codes starting with P: parks, recreation centres, the Stampede grounds). That is $27.8B of value in 2026. Its land still counts toward the hexagon it sits in.
Calibration. The same method applied to the 2025 roll and 2025 rates (where the exempt codes are IS and RC) gives $3,473M of total property tax, municipal plus provincial. The City reported $3,471M for 2025, a difference of under 0.1%.
3. Roads: how much pavement each hexagon has
We measure the length of City-maintained, paved road in each hexagon and multiply it by a typical width to get square metres of pavement. Citywide that comes to 6,288 km of road and 62.1 km² of pavement.
Which roads count. Roads come from OpenStreetMap (2025 extract). We keep five classes of OSM highway tag and drop the rest:
| Class | OSM tags kept | Length | Width used | Pavement |
|---|---|---|---|---|
| Highway | motorway, trunk and their links | 335 km | 11.0 m | 3.7 km² |
| Primary (arterial) | primary, primary_link | 226 km | 11.0 m | 2.5 km² |
| Secondary (arterial) | secondary, secondary_link | 909 km | 11.0 m | 10.0 km² |
| Tertiary (collector) | tertiary, tertiary_link | 1,162 km | 11.0 m | 12.8 km² |
| Residential (local) | residential, living_street | 3,656 km | 9.1 m on average | 33.1 km² |
Left out: lanes and alleys, service roads, unclassified roads, private roads, paths and tracks. Also left out:
- Unpaved roads: OSM
surfaceof gravel, dirt, compacted and similar. Roads with no surface tag, about a third of the network, are assumed paved. - Provincial roads: Stoney Trail (Highway 201) and Deerfoot Trail (Highway 2), which the Province maintains. Ramps within 300 m of them are dropped too.
Width. Divided roads are drawn in OSM as two separate lines, one per direction, so the bigger classes use the width of one carriageway, 11 m. That figure is a placeholder, not a measured value. Residential widths come from 110 sampled streets, 10 in each community structure. Within each structure, 10 random points were placed and snapped to the nearest residential road, and the road width at each point was measured with the Google Maps "Measure distance" tool. Each hexagon uses the median of its community structure's samples, from about 8.8 m in post-2000 suburbs to 10.3 m in Centre City. Where a structure has no samples, the 9.0 m default applies.
Shared roads. Highways and arterials (primary and secondary) are used by everyone, not just the people who live beside them. So their 16.2 km² of pavement is pooled and split equally across all 1,683 hexagons, about 9,600 m² each, instead of being charged to whichever hexagon they run through. Collectors and local streets stay with the hexagon they are in.
4. Cost: pricing the pavement
Each pavement plan turns square metres of pavement into dollars a year, in one of three ways. Where a plan gives a low and a high figure, the headline uses the midpoint.
City budget plans are a single citywide amount a year. Each hexagon gets the share that matches its share of the city's pavement:
That works out to the same dollars per square metre everywhere, whatever the road class.
Maintenance-cycle plans give a cost per square metre per year, applied to all pavement:
Full replacement is a one-off rebuild cost per square metre, spread over each class's life. Local streets last 40 years; every other class lasts 21.
| Plan | Type | Input | Citywide, a year | Source |
|---|---|---|---|---|
| Historical spending | Budget | $40M a year | $40M | Pavement Quality report, p. 7 |
| 2026 budget | Budget | $64M a year | $64M | Budget 2026, pp. 76–78 |
| Break even | Budget | $88M a year | $88M | Pavement Quality presentation, p. 7 |
| 50% of roads in good condition | Budget | $116M a year | $116M | Pavement Quality presentation, pp. 7–8 |
| 60% of roads in good condition | Budget | $132M a year | $132M | Pavement Quality presentation, p. 7 |
| Seal cracks every 10 years | Cycle | $1.00–2.50 per m² a year | $109M | Pavement Quality report, p. 4 |
| Repave every 12–17 years | Cycle | $2.92–4.17 per m² a year | $220M | Pavement Quality report, p. 4 |
| Full replacement | Rebuild | $300–350 per m², over 21 or 40 years | $717M | Pavement Quality Level of Service report, 2024 |
The 2026 budget is the current scenario: it is what the City spends now, and the yardstick for the tax increase in section 5.
Per-square-metre costs are in 2024 dollars. The pipeline can inflate them to 2026, but the inflation rate is set to zero, so no adjustment is applied.
5. Comparing with the mobility budget
Each hexagon's road cost is measured against the part of its own tax bill that goes to mobility: 6.62 cents of every municipal tax dollar, or $169M citywide.
Where 6.62% comes from. The City's property tax calculator breaks the full bill into services. Mobility is 3.87% of the whole bill, which includes the Province's 41.49%. As a share of the City's portion alone, that is 3.87 ÷ 58.51 = 6.62%. The City's full Mobility budget is larger ($377M in 2026) because it also draws on fees, grants and other revenue. Those are left out here, because property tax is the lever the City and each neighbourhood control.
The hexagon's mobility budget and the overrun:
A ratio of 0.4 means roads use 40% of the area's mobility budget, leaving 60% for sidewalks, bike lanes, streetlights, signals and snow clearing. A ratio of 3 is shown as a 200% overrun. The map colours hexagons at ratios of 0.5, 1, 2, 4 and 8.
Cents per tax dollar is the road cost over all municipal revenue, Ch ÷ Rh, set against the 6.62¢ line. For a group of hexagons (a ward, or a type of neighbourhood) it is total cost over total revenue, not an average of the hexagons' ratios.
Tax increase needed. This is how much the municipal tax rate would have to rise to pay for a plan, compared with the 2026 budget, if nothing else changed. It is never shown as negative.
Per median household. The median 2026 assessment of a single residential property (code R110) is $718,000, which pays $2,793 in municipal tax. The extra cost is that bill times the increase:
Overrun counts include only ordinary neighbourhood hexagons that pay some tax: 1,236 in 2026. Parks, undeveloped land, special districts and hexagons with no taxable property are left out.
6. Assumptions and limitations
The model counts only road pavement against only municipal property tax, so it understates what a neighbourhood really costs. Every other piece of infrastructure, and the rest of the mobility budget, comes on top.
What is counted
- Road pavement only. Not sidewalks, curbs, bridges, signals, lighting, snow clearing, pipes, transit or any other service.
- Municipal property tax only. Not provincial education tax, fees, grants, franchise fees or other City revenue.
- Mobility's 6.62% share is taken as fixed. Some road capital may also come from the separate "Contribution to Capital Investments" line (8.94% of the bill), so the true tax share for roads may be a little higher.
Road assumptions
- OpenStreetMap stands in for the City's road inventory. Its classes, surface tags and ownership are not official.
- The 11 m width for highways, arterials and collectors is a placeholder. Undivided four-lane arterials are likely wider. As a check, 62.1 km² of pavement over the City's roughly 17,400 lane-km works out to about 3.6 m a lane, which is plausible.
- Budget plans spend the same per square metre on every road class. The City actually prioritises by class, and its plans leave local streets in worse shape than arterials, so these plans likely overstate spending on local streets.
- One cost per square metre applies to every class. Arterial work likely costs more per square metre.
- Sharing highways and arterials equally across hexagons is a choice. The pipeline can also share them by area or by revenue, or leave them in place.
- Lifespans of 40 years (local) and 21 years (all others) apply to full replacement only.
- No inflation is applied to the 2024 unit costs.
Tax assumptions
- Exemptions are set by sub-type code groups (institutional and recreation), which match the City's 2025 totals but may include or miss individual properties.
- Value is spread by land area, so a tower's value is spread evenly over its parcel. Large parcels that straddle hexagon edges, such as malls or campuses, are split by area rather than by where the buildings are.
- Rates are applied directly to assessed value. Phase-ins, rebates, supplementary assessments and appeals are ignored.
- The tax increase assumes the whole gap is paid by a higher rate on the same tax base, with nothing else changing.
Grid assumptions
- Each hexagon takes one community and ward, from its centre point, even if it straddles a boundary.
- Edge hexagons are smaller, so they hold less road and less property than full ones, but still get an equal share of the shared roads.
7. Reproducing it
The whole method is a set of SQL scripts run in order by one Python script, with every setting in one file. Code: github.com/wklumpen/citymath.
Inputs
| Input | Source | Notes |
|---|---|---|
| Hex grid | Project file citymath.gpkg, layer hex_800m_clipped | 800 m hexagons built in QGIS, clipped to the city limit |
| Communities | City of Calgary community boundaries, 2025 | Name, community structure (era) |
| Wards | City of Calgary ward boundaries, 2025 (data.calgary.ca, dataset tz8z-hyaz) | Current 14 wards |
| Roads | OpenStreetMap, 2025 extract | highway, surface, ref tags |
| Assessments | City of Calgary property assessments (parcel), 2026 roll; 2025 historical roll for calibration | Value by class, sub-type code, roll number, parcel shape |
| Street widths | Project sample of 110 residential streets | 10 random points per community structure, measured in Google Maps |
| Tax rates | City of Calgary 2026 (and 2025) municipal and provincial rates |
The source data is not in the repository; only the derived per-hexagon files for the website are.
Steps
- Load the sources into a PostGIS database and list their table names in
config.yaml. - Put the database login in
.env. - Run
uv run citymath.py. It loads the grid, tags communities and wards, measures roads, allocates assessments, then builds the summary and plan tables (sql/00_staging.sqltosql/05_hex_scenario.sql). - The last step,
export, writes the website's data files todocs/data/.
After changing a setting, re-run only the later steps, for example uv run citymath.py roads summary.
Outputs. One row per hexagon in hex_summary, with community, ward, road length by class, pavement, assessed value and revenue by class, road cost and net. hex_scenario adds one row per hexagon per plan. Both open directly in QGIS.
Website and slides. The website recomputes each plan in the browser from the exported files, with the same formulas as above. uv run slides.py draws the presentation charts from the same files.