Build a trams-only city outside France and Czechia - the ten T1 cities in seven countries (Odense, Daugavpils, Liepāja, Kansas City, New Orleans, Tucson, Florence, Zurich, Göteborg, Den Haag) - with what every trams-only map shares - the owner's trams-only calls, rings by the spacing rule, no stop thinning anywhere (New Orleans included), gate 3 against the operator's own stop counts in every city, the light-rail test, the shared OSM tram step 1, rolling feeds, OSM rail, the currency rule, one-bucket and narrowed pages, the macro map's mode and coverage keys, and the page text the owner approved - plus a sheet per city pointing at its country's built template. Read with the city's brief, add-city, osm-rail and publish-city, which it does not replace.
Install
npx skillscat add dacekroberts/expanded-heatmap/tram-city Install via the SkillsCat registry.
Building a trams-only city (the ten non-French, non-Czech T1 cities)
Written 2026-09-30 by the tram kit session (docs/handoff_tram_kit_2026-09-30.md),
from the built trams-only and tram-heavy cities (Riga, Dublin, Aarhus and the
French five) and the owner's calls of 2026-09-27 to 09-30. France hasfrance-tram-city and Czechia its own kit. These ten cities are spread over
seven countries, one or two per country, so this skill carries only what they
share. Anything about one country is in that city's brief
(docs/build_briefs/<slug>.md, which staging writes) and in the built city the
brief names as its template. japan-city, taiwan-city and brazil-city stay
the model for a country skill. Write one only if a country here gets a third
city.
Builds are HELD (owner, 2026-09-30) until the owner gives the go. The
kit's 24 calls were approved on 2026-09-30, but that is not the go. Build on a branch, never on master: app/ lands
at review time only (docs/review_time.md).
Order of work for one city
python scripts/brief_check.py <slug>. A failing claim means the brief
needs correcting. Exception: "MIRRORS DISAGREE" is an Overpass host
problem, so re-run it (Liepāja, 2026-09-30).- Read the brief's "For the owner, with the build" table:
mode,coverage, scope, lines, rings and each call with its recommendation. If
the table is missing, ask staging for it before writing code. - Zurich only:
add-countryfirst (Switzerland is new; see its sheet). add-cityStep 0 is already in the brief. Go toscaffold-city:scripts/scaffold_city.py ... --mode <mode> --dry-run, then the real run.
There is no batch scaffold (DECISIONS "The tram kit: no batch scaffold").
Then copy the template city's config for everythingscaffold_city.py
leaves generic. The sheet below names each city's template.- Set the rings (section 3) by hand, and the CRS where section 3 says to.
scaffold_city.pywrites the standard rings and a WGS84 UTM zone. - Find the operator's own per-line stop counts (section 2, "Gate 3")
before writing step 1, and put them in config, or record why none exist.
The brief's station count is not one: it is usually OSM's. - Write
fetch_sources.py, which always downloads the feed (section 4),
then step 1, step 2 and the render. - Fill the page
scaffold_city.pywrote (it is already in the city-page
format) from section 6, and write the city's sections ofdocs/excluded_categories.mdanddocs/data_sources/<country>.md. The
format for all three isdocs/city_page_format.md; never start a page by
copying an older one. Then run "Before publishing".
The owner's standing calls - do not re-ask
Trams-only maps are approved (DECISIONS "Yes to trams-only maps",
2026-09-29). With no metro, the trams are the rapid transit (Riga's
precedent), and every tram stop gets rings.No stop thinning on the tram list (DECISIONS "Tram audit Phases 3-4",
owner). Riga's 0.5 mi filter does not carry over, even to the two Latvian
cities built on Riga's modules. New Orleans keeps every stop too
(owner, 2026-09-30, call 13), so no city on this list is thinned.Rings follow the spacing rule (DECISIONS "Tram cities use the existing
spacing rule", owner, 2026-09-29), section 3.The light-rail test (
docs/tram_city_list.md, owner, 2026-09-29) decides
whether a system is a tram at all, and it decidesmode(section 1).Feeds are rolling: fetch at build, never cache one across weeks.
The currency rule (the master list's "Five rules", owner, 2026-09-29):
the source drops closed businesses, its newest row is under five years old,
and the data date goes on the page. Kansas City (frozen 2026-01-15) and
Göteborg (undated rows) carry it in the page text (section 6).Denmark's placement is OSM's DAR address points (owner, 2026-09-27),
keyless and under ODbL. The Datafordeler account is reopened only if a build
proves it needs one.Rejseplanen's GTFS is never used (owner, 2026-09-29, for Aarhus).
Zurich's partial retail is the alcohol-licensed shops, on the tobacco-
retail precedent of Seoul and Gyeonggi, with the gap disclosed (owner,
2026-09-30).Den Haag's horeca layer is used on Amsterdam's precedent (owner,
2026-09-30): credit "Gemeente Den Haag", and never call the layer current or
complete.A GTFS feed that declares no licence is not used: take OSM (Olomouc,
owner, 2026-09-30). Daugavpils and Liepāja follow it.RideKC's GTFS is not used (owner, 2026-09-30): Kansas City's rail is
OSM.The kit's 24 calls, approved as recommended (owner, 2026-09-30: "Agree
with recommended options for all"; the numbers are the handoff's):- the page-text template (section 6);
- a route is drawn only if it runs at least every 20 minutes by day, and
a stop served only by slower routes is listed as infrequent (section 1); - the project's own colours where the source has none (Odense, Daugavpils,
Liepāja, Kansas City, Tucson); - Odense's SDU Syd/Hospital Nord added by node, and Hospital Syd out until
it opens; - Odense's
modeistram; - Daugavpils's routes under the call 2 floor (superseded by call 28:
every route is drawn); - Daugavpils and Liepāja are
narrowed, "Merged"; - notice 42 extended to name VZD's address register, in VZD's wording
with the year; - Liepāja's Brīvības iela and Klaipēdas iela added by node;
- Kansas City keeps licences valid for 2025 and 2026 only;
- Kansas City's
dba_namewithheld where it reads as a person (Houston's
rule); - Kansas City's fee-code types dropped, with the count on the page;
- New Orleans keeps all 110 stops;
- New Orleans drops "Special Events-Other (Vendor)" and "Home
Based-Office Use Only"; - Tucson's
ACC_NAMEwithheld for Sole Proprietorship, Individual and
Married; - Florence keeps its 639 exempt food rows, filtering what the type code
names as non-public, on Milan's fuori piano precedent; - Florence is commune-only, with T1's 4 Scandicci stops listed as
outside; - Zurich's Forchbahn is left out and named on the page;
- Zurich's Glattalbahn lines each take the stub test, and a stub is
dropped; - Göteborg's blank-
typrows are classified by name where the name
shows a counter, and the rest dropped; - Göteborg's lines 4 and 12 are drawn to their ends, with the Mölndal
stops listed as outside, unless the stub test fails; - Den Haag's RandstadRail E is left out as a stub;
- Den Haag is
narrowed, "Merged"; - Den Haag's 158 pending horeca permits are left out.
Calls 18-24 were approved before their briefs landed. If a brief's
measurement changes the facts behind one (a Glattalbahn line that is not
a stub, a Mölndal line that is), the call goes back to the owner.The four calls from the last briefs (owner, 2026-09-30):
- Zurich's tram 20 (Limmattalbahn, 4 of 26 stops inside) is left out as
a stub; - Den Haag's tram 1 (19 of 37 inside, 51%) is drawn to its end, with the
stops outside listed; - Den Haag's page carries the permit layer's date (section 6);
- Daugavpils draws its whole network, routes 1-5, and the page states
the waits. In the owner's words: "like with buffalo 50% of network
shouldn't fall if the norm is longer waits." Call 2's floor would have
kept only route 1, 19 of the 38 stops.
- Zurich's tram 20 (Limmattalbahn, 4 of 26 stops inside) is left out as
1. Is it a tram, and what is its mode?
The light-rail test (docs/tram_city_list.md) has three parts, read
against San Diego, Calgary and Edmonton:
- Track: tunnel and bridge share, and OSM's
light_rail/tramsplit. - Frequency: every 15 minutes or better by day. This is a gate only on
converted railway (Aarhus's L1). On purpose-built track a slower timetable
is disclosed on the page (Buffalo's flat 20). - Spacing: about 550 m or more. This supports the verdict but is not the
gate.
A city that passes left the tram list on 2026-09-29. All ten here stayed,
so they are trams, and mode is "tram" unless step 1 keeps something of a
higher order. mode is the highest-order mode the map DRAWS, decided from
what step 1 keeps (GTFS route_type, OSM route=), never from a marketing
name. France's feeds showed that mode flags lie (Reims's tram is typed metro).
Here that means:
| City | mode |
Why |
|---|---|---|
| Den Haag (EDGE) | tram (the brief, 2026-09-30: every drawn line, RandstadRail 3 and 4 included, is OSM route=tram) |
Line E is left out as a stub (owner, call 22): 4 of its 23 stops are in the city, as Ostrava's line 5 and Madrid's ML2 and ML3 were. So metro is never drawn |
| Zurich | tram |
The Forchbahn (S18, OSM route=light_rail) is left out (owner, call 18) and named on the page with the S-Bahn. Inside the Stadt it runs on tram 11's track and stops, so drawing it would add no rings |
| Odense | tram (owner, call 5) |
Aarhus, its template, is light_rail (its new tramway passed the test). Odense's Letbane is OSM route=tram and street-running, and it stayed on the list |
| the other seven | tram |
none |
Frequency: disclose the network's norm, drop only an outlier (owner,
calls 2 and 28). Street trams have no frequency gate. Read each route's
daytime headway from the operator's own timetable.
- Where longer waits are the network's norm, every route is drawn and the
page states the waits (call 28, Buffalo's precedent). Daugavpils is the
case: route 1 every 10-15 minutes, the Stropu loop (3 and 5) every 20-30
minutes each way, routes 2 and 4 about hourly. All five are drawn, because
dropping the slower routes would take half the network (19 of 38 stops). - Call 2's 20-minute floor (Buffalo, the slowest drawn) is for an outlier
route: a slow route on a network that otherwise meets it, such as
Zurich's 50 and 51 if they turn out not to run by day. Its stops, if no
other route serves them, go toexcluded_stations.csvas infrequent,
Aarhus's class (app/station_scope.py), and the page says so. - The line between the two is the owner's. When a slow route is more
than an outlier, bring the share of stops it alone serves.
2. Stations - every stop
- Every stop in scope gets a ring. Collapse platforms to stations and
thin nothing.pipeline/stations.py's gates still run: spacing, per-line
counts, and the boardable filter. - GTFS feeds: take the feed's own
parent_stationrow first. Without
one, collapse by unchanged name as the template city does. On an ODbL
feed, France's pure-extract rule applies (france-tram-citysection 2):
the first platform instops.txtorder, never a mean, rename nothing. The
rule came from the French portal's ODbL conditions, so it does not reach
a CC0, CC BY or public-domain feed. A rename or a merge of two differently
named stops goes to the owner. - OSM rail: route-relation membership, never a tag search (
osm-rail).
One Overpass query per city. Bound every name search with a bbox. Two
relations per line, so draw the one with the most way geometry
(map_common.load_osm_line_shapes). The collapse is Aarhus's
(pipeline/aarhus/step1_stations.py): stop members collapsed by name at
their mean within 200 m, exiting if a name spreads wider.- A tagged stop missing from every route relation is added by its node,
named in config (STATION_ADD). Odense's SDU Syd/Hospital Nord
(opened 2023-08-25) and Liepāja's Brīvības iela (the named terminus) and
Klaipēdas iela are the cases. Check each against the operator's own stop
list. - A stop that is not yet open stays out and becomes a watch item
(Odense's Hospital Syd, 2027).
- A tagged stop missing from every route relation is added by its node,
- New Orleans keeps every stop (owner, 2026-09-30, call 13). Its
streetcar stops stand a block or two apart (164 m median), so the rings
merge into a band along each line. The kit's handoff had named it the one
exception for the street-stop filter, but the filter's own shape test
(docs/sub_transit_line_filters.md, "When this applies") fails. That
filter is for a central corridor with sparse stations plus dense surface
branches (Muni Metro, Boston's Green Line). New Orleans is uniformly
dense, with no corridor, the case the doc says not to thin. Thinning
would also leave a business a block from a stop outside the rings. The
band is a true reading of distance from the line, and the page says so
(section 6). - A line mostly outside the scope is a stub question, as for the Czech
and French cities. Decided (owner, 2026-09-30):- Florence's T1 is drawn to its end, with its 4 Scandicci stops listed as
outside (call 17); - Den Haag's E is left out as a stub (call 22);
- Zurich's Glattalbahn lines take the stub test one by one, and a stub is
dropped (call 19); - Göteborg's 4 and 12 are drawn to their ends, with the Mölndal stops
listed as outside, unless the stub test fails (call 21).
Where the business source covers the city only (Zurich, Göteborg, Den
Haag's horeca layer, Florence), the scope cannot go regional, so
France's scope rule does not apply.
- Florence's T1 is drawn to its end, with its 4 Scandicci stops listed as
Gate 3 - the operator's own stop count, every city
Required, not optional. pipeline/stations.py's gates 1 and 2 (spacing,
boardability) run by themselves. Gate 3 runs only when step 1 passes the
operator's figures in, and in the first ten builds it ran once (Odense,
25 of 25). Four drafts said "No gate 3" and five said nothing (the tram kit
retrospective, docs/tram_kit_retrospective.md). It is the one check
outside the data: Toronto's 118 and 77 agreed with every internal check and
with nothing the TTC published. On OSM rail it matters more, not less.
OSM is a volunteer map, and the brief's station count is usually OSM's own.
Config carries it, as Odense's does (
pipeline/odense/config.py):OPERATOR_STATION_COUNTS = {ref: n}, one entry per drawn line;OPERATOR_COUNTS_SOURCE, which names the page or document, its
publisher and the date read.
Step 1 passes it toverify_stations(expected_per_line=...), with the
per-line counts taken from the collapsed stations
(odense/step1_stations.py).
Count the whole line, before the scope split. An operator lists every
stop on a line, including those past the city boundary (Florence's T1 into
Scandicci, Göteborg's 4 and 12 into Mölndal). Compare per line, an
interchange counting once on each line it serves.Reconcile in config, never by loosening. Where the operator's figure
and the build differ for a known reason, write the reason beside the
figure. Odense: "26 stops less Hospital Syd, not yet open". Other known
reasons:- a
STATION_ADDstop the operator lists; - a stop the operator counts per direction;
- a temporary line's stops for the current timetable period (Zurich's 50
and 51 run to 2026-12-12).
- a
verify_stationsprints a mismatch; a tram step 1 stops on one. Wrap
it like this:res = station_gates.verify_stations(...) if res["per_line_mismatches"]: sys.exit(...) # name each line with both figuresDo not change
stations.pyto raise: other cities call it.The source must be independent of OSM. In order of preference:
- the operator's own line pages, stop list, network map or timetable
PDF, read for the count only, never republished; - a public authority's page for the line;
- a dated secondary source (English Wikipedia's table of lines, as
Stockholm's gate 3), named as secondary inOPERATOR_COUNTS_SOURCE.
These are never the source:
- OSM, or a brief count derived from OSM. That is circular, as Kansas
City's station check was: it compared OSM against the brief's 19, which
came from OSM. - A feed or site the owner barred (RideKC's GTFS and its schedules). Look
instead for another publisher of the same line (for the KC Streetcar,
the KC Streetcar Authority's own site, if it lists the stops).
- the operator's own line pages, stop list, network map or timetable
Where nothing usable is published, say so in config:
OPERATOR_STATION_COUNTS = None, plusOPERATOR_COUNTS_GAP, which records
what was looked for, where, and the date. The draft entry repeats it, and
the drafts file and review time list it as a gap. A silent skip is the
failure this section exists to stop.Back-filled 2026-10-01 in the nine built without it, with 16 earlier
cities (DECISIONS, "Gate 3 back-filled in 25 cities"). It found real gaps
in New Orleans, Daugavpils and Den Haag, each OSM missing a stop the
operator serves: those step 1s print the mismatch with the exit commented
out until the owner's station fix (PLAN.md, "Gate 3's station calls").
A new tram build runs gate 3 before it is committed. A mismatch is a
station fix with a drift check, not a page edit.
3. Rings - the spacing rule
Measure the median nearest-neighbour gap among the stations in scope,
after collapse, with every stop kept.
- About 550 m or less:
RING_EDGES_MILES = [0.0, 0.05, 0.1, 0.2, 0.3],RING_LABELS = ["0-0.05 mi", "0.05-0.1 mi", "0.1-0.2 mi", "0.2-0.3 mi"]
(Aarhus's config). The config comment records the figure. - Over about 550 m: the standard
[0.0, 0.1, 0.2, 0.3, 0.6]. Drop the
page's half-size sentence. A build figure between 540 and 570 m goes to the
owner with both in-ring shares. Kansas City read about 560 m as a mean at the
screen, but 413 m as a median over OSM's 19 stops in its brief, so it
takes halved rings. - The briefs' gaps (2026-09-30): Odense 441 m, Daugavpils 298, Liepāja 329,
Kansas City 413, Florence 322, Tucson 265, New Orleans 164, Zurich 283,
Göteborg 378, Den Haag 335. All ten take halved rings; recompute on the
stations actually drawn. - Riga is on the standard rings with thinning. Its Latvian followers are
NOT: no thinning and halved rings, as above. - Distance in a metre CRS, never EPSG:4326, and the CRS is per city:
OdenseEPSG:25832(Aarhus's, DAR's own); DaugavpilsEPSG:32635;
LiepājaEPSG:32634(21.0° E, not Riga's 35N); ZurichEPSG:2056
(LV95; the register'sekoord/nkoordare already in it). The US,
Italian, Swedish and Dutch cities use their own WGS84 UTM zone, whichscaffold_city.pyderives from the longitude: Göteborg is 32N, not
Stockholm's 34N. Override the scaffold only for Odense and Zurich. - After the city lands:
python scripts/ring_rules_table.py --write.
4. Feeds are rolling, and OSM is the fallback
fetch_sources.pyalways downloads the feed, even whendata/<slug>/raw/holds one. Record the fetch time and the feed's ownfeed_info.txtwindow (or the publisher's metadata) inoutputs/<slug>/provenance.json, which the page's caption reads. A step
never fetches (check_no_fetch_in_steps.py).Read the licence before a feed is used, with one
licence-readper
source (read-licence). RTA's and GEST's GTFS are unread, and neither
brief uses them. RideKC's GTFS is barred (owner, 2026-09-30): ridekc.org's
site terms restrict schedules and require written consent, Philadelphia's
shape. So Kansas City's frequency is stated as a fact, without citing the
schedule, and the KC Streetcar and RideKC logos are never used.OSM through
osm-railis the rail source for every brief so far. It
is used where a feed is unlicensed, barred, or simply not needed for
geometry: Odense (Rejseplanen barred), Daugavpils and Liepāja (no declared
licence), Kansas City (barred), New Orleans, Tucson and Florence. Zurich,
Göteborg and Den Haag were measured on OSM at their screens. OSM is covered
by the site's OpenStreetMap notice. Say on the page that the lines and
stops come from OpenStreetMap.Relations with no stop members are placed in
NOT_DRAWNwith a reason,
never dropped silently: New Orleans's 46 and 49 (read whether they run),
and Florence's T3.2.1, T3.2.2, T2.2 and T4 (under construction; re-check
when T3 opens, due end of 2026).Colours: the feed's
route_color, else OSM'scolour. A CSS keyword is
resolved through the named-colour table: New Orleans'sgreen,redandblue, Göteborg's line 11 "black". Otherwise use the project's own
palette, as Le Havre and Riga have: Odense, Daugavpils, Liepāja, Kansas
City and Tucson. Runpipeline/linecolour.py's checks andcheck_map_markup.py. Two lines with one colour are refused (Dijon).Every drawn line gets a permanent on-map label (its real public name) and
a legend entry.Shared code where it pays:
pipeline/osm_tram.py, one module for both
kits (agreed with the Czech kit, 2026-09-30). Seven of these ten briefs
take OSM rail, the other three were measured on it, and five of the six
Czech cities do too. Use it. If it does not exist yet,
the first OSM tram build writes it, from Aarhus'sstop_rows()
generalised:- relations kept on
refandroute, and onoperatoronly where the
config names one; STATION_ADDby node id for a tagged stop on no route relation;- stop members tagged
public_transport=stop_positionorrailway=tram_stop, exiting on an untagged or unnamed member and naming
it (Daugavpils's Stropu ciemats is on the routes with no stop tag); - every matching relation is kept or listed in
NOT_DRAWNby relation
id with a reason, and the step exits on one that is neither (the Czech
screens found zero-stop refs, unref'd variants and depot runs); - several relations per ref: a line's stops are the union of its kept
relations, andload_osm_line_shapesdraws the one with the most
geometry; - scope over a union of polygons (Liberec + Jablonec), with stations
outside named with the place they lie in, as Rennes names communes; - a lines-only mode that selects and validates relations for the line
geometry without producing stations, for a feed with stations but noshapes.txt(Brno); - Aarhus's name collapse, and
pipeline/stations.py's gates.
Its control: run on Aarhus's inputs, it reproduces Aarhus's
stations.csv. Aarhus itself is not rewired, and the later OSM cities are
wrappers. The same goes for Latvia's step 2: Daugavpils's build lifts Riga's step 2
(226 lines) intopipeline/countries/latvia_register.py, and Riga'sdrift_check.pyis the control (Riga's outputs must not move). Shared
modules are the app/chrome role's paths (docs/session_roles.md), so tell
that role before writing one.- relations kept on
5. Coverage - full, narrowed or one bucket
The macro map's fill (coverage) follows the categories value (owner,
2026-09-30; the add-city skill on macro-legend): "All three" is full;
"Two", "Merged" and either "thin" are narrowed; "Food premises only" isone_bucket. Food shops are food. A token third layer does not lift a
city, and that is the owner's call per city (ONE_BUCKET_BY_OWNER).
| City | categories |
coverage |
Why |
|---|---|---|---|
| Odense | All three | full |
CVR, as Aarhus |
| Daugavpils, Liepāja | Merged | narrowed |
Riga's two layers (shops and services merged; food thin by construction) |
| Tucson | All three | full |
City licence register with all three |
| Kansas City, New Orleans | All three, one thin | narrowed |
City licence registers; Kansas City's food service and New Orleans's personal services are thin (categories "Food service thin", "Personal services thin"; DECISIONS 2026-09-30, A2) |
| Florence | All three | full |
The Comune's four layers |
| Zurich | Two | narrowed |
Food plus partial retail (alcohol-licensed shops, kiosks, petrol stations); no personal services. Boston's precedent: retail that is not only food shops makes two |
| Göteborg | Food premises only | one_bucket |
Food service plus food shops, which count as food |
| Den Haag | Merged | narrowed |
Rotterdam's shape (BAG winkelfunctie plus the horeca layer), which is narrowed, "Merged". Approved narrowed over the tram list's "full" (owner, call 23) |
A narrowed or one-bucket page says what is missing in its own bullets
(section 6), and check_scope_disclosure.py holds it todocs/excluded_categories.md, where the gap goes in the city's own section,
never the shared "What is missing rather than excluded" one
(docs/city_page_format.md, section 2). The layer control andrender_map_help name only the categories the map has (Riga's and Liepāja's
"two business categories (Shops and services, and Food service)").
6. Page text - approved by the owner, word for word (2026-09-30)
The page scaffold_city.py writes is already in the city-page format
(docs/city_page_format.md, section 1): render_city_title, the map, the
captions, the bullets below under bold headings, then render_map_help,render_excluded_stations, render_country_links and render_site_notices
last. The wording is the template as the ten built pages carry it after the
2026-10-01 prose pass (app/pages/130_Odense_Heatmap.py to139_Zurich_Heatmap.py). Braces are per-city facts, filled from this city's
own measurements (step 1, step 2 and the rendered map), never from the
brief's screen figures. A bullet in braces appears only where it applies. A
sentence from this template needs no read-back; one it does not cover is a
proposal, flagged in the session's drafts file
(docs/city_page_format.md, section 6).
The captions, directly under the map, from provenance.json: the
business source with its own date, then the rail's fetch date, for example
(Florence's and Göteborg's shape; a second source adds its own clause, as
Den Haag's BAG units do):
{Premises data} from {publisher}{ (license)}, {last updated / fetched}
{date}; the tram lines and their stops from OpenStreetMap, fetched
{date}.
The prose, as bullets under three bold headings. The first heading is
The trams, The tram for one line, The streetcar(s) for a US
streetcar.
The trams
- {N} {operator} tram lines are drawn, {lines}, each labeled on the map
and in the legend, redrawn from OpenStreetMap's route geometry{, in colors
this project chose, since the source records none / , in OpenStreetMap's
own colors}.- {Trams run about every {n} minutes by day{; less often in the
evenings}.}- {Buses{ and suburban trains} are not drawn{: why}.}
- {City} has no metro, so its trams are its rapid transit, as in Riga.
Every tram stop here gets rings.- Tram stops sit closer together than metro stations, a median of
{spacing} m here, so the rings are drawn at half the usual size (0.05 to
0.3 mi).- The map covers the {city unit}. {Where a line runs past it: "{Line}
runs on into {place}, so its {k} stops there are left out."}- {Where a line runs past it: "The line is still drawn to its end, but those
stops get no ring and their businesses are not counted. They are listed
below."}The businesses
- {The business source, in the template city's wording: which register, how
the dots are placed, and whether a dot shows a name, a type or an address,
one claim to a bullet.}- {Narrowed: "This map has two categories, not three. {What the source
holds, and what is missing.}" One bucket (Göteborg): "This map shows
food only, not three categories. {What the source holds.}"}- {Kansas City: "The license data dates from 15 January 2026, and holds
licenses valid for 2025 and 2026. Businesses that opened since then are
missing, and any that closed may still be shown."}- About {two storefronts in five} sit within a ring.
Reading the density
- {Göteborg: "The register carries no dates. It lists the food
businesses active on the day it was fetched ({date}), and says nothing
about when each opened." Den Haag (owner, call 27): "The permit data
runs to 2025. The city last edited its permit layer on 23 May 2025, so
premises that opened or closed since then may be missing or still
shown."}- Read the density as a register, not a street survey. {The source's
own caveat.}- {New Orleans: "Streetcar stops here stand a block or two apart, so the
rings join into a band along each line. Read them as distance from the
line."}
Notes on the template:
- The in-ring share is said in words, as on all ten pages: "About two
storefronts in five sit within a ring", "About one storefront in eight
sits within a ring". All ten put it last under The businesses, and
all ten name the third heading Reading the density (the family wording
batch, 2026-10-03). - A US streetcar says it the way Kansas City's, Tucson's and New Orleans's
pages do: "{City} has no metro: its streetcar is its rapid transit, as
Riga's trams are, so every stop gets rings", and "Streetcar stops sit
closer together than metro stations ...". Their scope bullet adds ", as
the US Census Bureau draws its limits. Every streetcar stop is inside it." - The stops left out are "listed below":
render_excluded_stationslists
them on the page. Never a repository path, script or check name in page
text. - No ratio to OpenStreetMap: that comparison is the French pages', where
SIRENE counts registered establishments; no tram-kit page carries one
(owner, call C2, 2026-10-01). - No controls paragraph or heat caveat of the city's own:
render_map_help("{three business categories (Retail, Food service and Personal services) / two business categories (Shops and services, and Food service) / ...}")renders them, with the categories this map has.
A city whose median stop gap is over about 550 m keeps the standard rings
and drops the ring bullet. Where the rail comes from a feed rather than
OpenStreetMap, the first bullet names the feed ("from {operator}'s own
published timetable feed"), as France's template does.
Departures are owner calls, never edits. Anything that does not fit the
template goes to the owner as a proposed sentence.
7. The macro map and the app entry
- Two keys (owner, 2026-09-30, landed with the macro-legend branch):
colour =mode, fill =coverage, as sections 1 and 5.scaffold_city.py --modeis required and writesmodeinto thecities.pyentry;cities.pyraises at import without it. - Region:
"Europe"for the seven European cities."United States West"
for Tucson;"United States East"for Kansas City and New Orleans, since
Houston (−95.4°) is East.--countryis spelled asapp/cities.pyspells
it, and"Switzerland"is new. - The label width is measured in a real browser for
check_macro_labels.py.
Landing is in groups at review time with acity-addeddeploy-verify per
group.app/changes reboot the app.
Before publishing (each city)
python scripts/check_personal_exposure.py <slug> (verdict in the session's
drafts file and docs/privacy_verdicts.md;
the suspects are Kansas City's dba_name, New Orleans's ownername,
Tucson's ACC_NAME on personal ownership types, Florence's beauty and
laundry points (no names, but sole traders' premises) and Den Haag'sAANVRAGER);
gate 3 ran against the operator's per-line counts with no unexplained
mismatch, or OPERATOR_COUNTS_GAP records why it could not (section 2);check_provenance.py names the city OK; check_scope_disclosure.py passes;
a row in docs/data_sources/<country>.md for every new source, the join and
address layers included (VZD's aw_eka.csv); the page and the city's
reference-doc sections read as docs/city_page_format.md sets out (headings
that name the city, its gaps in its own section, process notes out or inside<!-- internal --> markers, no repository path on the page);drift_check.py <slug>; no TODO left in pipeline/<slug> or app/; a
decisions entry in the session's drafts file
(docs/decisions_drafts/<session-or-branch>.md, decisions-entry). Thenpublish-city.
Heavy jobs (announce to every live session first, docs/session_roles.md):
Odense's step 2 reads the national CVR cache, and the Latvian step 2 reads
the national excise file and VZD's 141.7 MB address file. The rest are
city-sized.
The ten, one sheet each
Proposed values are the briefs' or, where a brief is not yet written, the
tram list's. The briefs are staging's. Where they differ, the brief wins, and
the call list in docs/handoff_tram_kit_2026-09-30.md collects the owner's
calls.
| City | Template (built) | Business leg | Rail source | CRS | Traps |
|---|---|---|---|---|---|
| Odense | Aarhus (pipeline/aarhus/, countries/denmark*.py, taxonomies/denmark_db25.py) |
CVR, kommune 461; placed on OSM's DAR points (98.4%); personally owned rows show the address, as Aarhus | OSM, 2 route=tram relations, ref L; + SDU Syd/Hospital Nord by node |
25832 | No colour in OSM; Hospital Syd opens 2027; build on the shared CVR cache, never refresh it from a branch |
| Daugavpils | Riga (pipeline/riga/, taxonomies/riga_source.py) |
VID excise (food, placed on VZD's aw_eka.csv) + VZD cadastre use class 1230 (shops and services), ATVK 0002000 |
OSM, routes 1-5, all drawn (owner, call 28): 38 stop names; the page states the waits (route 1 every 10-15 min, the Stropu loop 20-30, routes 2 and 4 about hourly) | 32635 | aw_eka.csv's KOORD_X is the northing (EPSG:3059); use DD_N/DD_E; STATUSS = EKS; the VZD credit (notice 42 extended); hourly routes (section 1) |
| Liepāja | Riga | as Daugavpils, ATVK 0005000 | OSM, ref 1; + Brīvības iela and Klaipēdas iela by node | 32634 | as Daugavpils |
| Kansas City | Houston (a US register, the sole-owner name rule); naics.py through a NAICS 2022 title map |
Socrata kkhs-93m4, Public Domain, frozen 2026-01-15; valid_license_for 2025 and 2026 only; fee-code types ("Misc Rate 129") dropped and counted |
OSM (RideKC's GTFS barred, owner), ref 601, 19 stops, all inside; 413 m | 32615 | The data date on the page; dba_name withheld where it is a person; no logos; frequency as a fact, not a cited schedule |
| New Orleans | Philadelphia (a US text taxonomy, premises-taxonomy) |
Socrata iqay-p646, CC0, daily; drop "Special Events-Other (Vendor)" and "Home Based-Office Use Only"; ownername never shown |
OSM, streetcars 12, 47, 48 and 2, 110 stops, all inside; 164 m | 32615 | Every stop kept (owner, call 13); 46 and 49 have no stops (NOT_DRAWN unless they run); CSS-keyword colours |
| Tucson | Houston (the sole-owner name rule); naics.py unchanged |
BUSLIC layer 3 (never layer 1), active (strip LIC_STATUS) and HOME_OCCUPATION = F; licence SILENT, permissive reading (owner, 2026-09-30) |
OSM, Sun Link, 21 stops, all inside; 265 m | 32612 | Notice "Business licence data: City of Tucson"; never call the pins complete; ACC_NAME withheld for Sole Proprietorship, Individual and Married; 10 min weekdays 07-18, 20 otherwise, disclosed |
| Florence | Milan and Rome (the Comune's layers; Milan's fuori piano) | Four Comune GeoJSON layers (datigis.comune.fi.it/json/), EPSG:3003, no names or addresses (the dots show the type); licence read: CC BY 4.0, credit the Comune di Firenze and state the changes |
OSM, T1 and T2 in OSM colours, 39 stops in the comune (T1 20 of 24); 322 m | 32632 | 639 exempt food rows kept, non-public ones filtered (owner, call 16); Scandicci's 4 stops outside; 3,424 rows share a point; T3 under construction (NOT_DRAWN); no giglio or logo |
| Zurich | Stockholm (one register, city only), Seoul/Gyeonggi (partial retail) | Gastwirtschaftsbetriebe via the WFS (the CKAN downloads are an Angular shell), layer gastwirtschaftsbetriebe, CC0, 3,487 rows all Offen and 2026: food 2,325 + partial retail 1,028 (Kleinverkaufsstelle, Kiosk, Tankstelle) |
OSM: trams 2-11, 13-15, 17, 182 stops; 283 m. Out as stubs: Forchbahn S18 (20%), Glattalbahn 12 (11%), Limmattalbahn 20 (15%); 50 and 51 only if they meet the 20-minute floor | 2056 | add-country first; the S-Bahn and Forchbahn named as excluded |
| Göteborg | Stockholm (taxonomies/sweden_livsmedel.py) |
Livsmedelsverksamheter, CC0, daily: take the CSV (utf-8-sig, ;), never the rowstore JSON; 5,066 rows: food service 2,167, food shops 906; 274 blank typ classified by name (about 74 kept) or dropped |
OSM: trams 1-13, 127 stops; 378 m; 4 (75%) and 12 (72%) drawn to their ends with five Mölndal stops outside; Lisebergslinjen out | UTM 32N (11.97° E; not Stockholm's 34N) | Undated rows (the page says so); line 1's OSM colour is white (check contrast; Lille's darkening if it fails); take the hex where a relation has two colour values |
| Den Haag | Rotterdam (BAG, taxonomies/rotterdam_source.py); Amsterdam for the horeca precedent |
BAG winkelfunctie units (6,560) + Horeca_nieuw layer 2 (2,543 granted or notified, about 2,352 food after exclusions; edited 2025-05-23) |
OSM: 14 HTM lines, 161 stops; 335 m; RandstadRail E out; tram 1 keeps 19 of 37 (51%) | UTM 31N, as Rotterdam | Never fetch AANVRAGER, KVKNUMMER or RECHTSVORM; the trade name is OMSCHRIJVI, double-encoded UTF-8 to repair; 1 and 19 share #c01115 (one moves); 10 and 34 need colours; the layer's date on the page |