Skip to content

Releases: alexwohlbruck/portolan

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 14 Sep 16:59
54e9b2a

Fare gates, said out loud

A router decides whether a rider can walk between two stations and carry
on, and almost every router infers that from geometry, because most feeds'
transfers.txt is incomplete. For a gated railway the inference is wrong in
a way that costs money: Borough Hall and Jay St-MetroTech are 240 m apart
and the MTA deliberately does not connect them — you leave the paid area
and pay again — so a router that links stations by proximity offers the
walk as a free change and hides the one-fare alternative, which in that
case is no slower at all.

GTFS already has the answer: transfer_type=3, "Transfers forbidden
between routes at these stops". It is under-used because writing one row
per unconnected pair by hand is absurd, so portolan derives them:

"transfers": { "forbid_undeclared": true }

Every pair of GATED stations (metro, rail, funicular, monorail) within
walking distance that the agency's own transfers.txt does not connect is,
by the agency's own account, not a transfer. On the MTA subway feed that
keeps all 613 declared transfers and adds 278 prohibitions.

Bus, tram and ferry pairs are left alone: an undeclared walk between two of
those is settled by the tariff's transfer window, not by a gate.

allow and forbid lists state individual pairs by hand where derivation
misses one. A prohibition beats a transfer from any source, including the
feed's own row — a curator forbidding a declared pair is correcting the
feed, which is what the file is for.

Off by default: it is only safe for a feed whose transfers.txt is complete,
and asserting that is a curator's call.

Curated fares for feeds that publish none

GTFS has carried fares since the beginning and most agencies publish
neither v1 nor v2. The MTA is the extreme case: not one of its seven feeds
ships a single fare row, so a router reading them prices a trip across New
York at nothing at all.

A curation document can now state the tariff, and the exported feed carries
it as standard Fares v2:

"fares": {
  "currency": "USD",
  "price": 3.00,
  "transfer": { "count": 1, "minutes": 120 }
}

That renders fare_products.txt, fare_leg_rules.txt and — where a free
transfer is allowed — fare_transfer_rules.txt. The transfer rule is the
load-bearing one: a router that reads v2 groups an itinerary's legs into
fare transfers, so the rule decides how many times a rider pays, not just
how much a leg costs.

Deliberately a FLAT fare with an optional free-transfer window, which is
what the missing-fare feeds overwhelmingly charge. Zone, distance and
time-of-day tariffs stay the agency's to publish: too big to hold by hand,
too easy to hold wrongly. A curated tariff replaces whatever fare tables the
feed shipped; a feed with no fares block passes its own through untouched.

style/mta-subway.json carries the MTA's tariff.

Install

tar -xzf portolan_0.5.0_darwin_arm64.tar.gz
cd portolan_0.5.0_darwin_arm64 && ./portolan help

Each archive holds the portolan binary, the style/ curation
documents it reads by default, and the reference docs. No runtime
dependencies — it is a single static binary.

Verify with sha256sum -c SHA256SUMS.

What's Changed

Full Changelog: v0.4.11...v0.5.0

v0.4.11

Choose a tag to compare

@github-actions github-actions released this 09 Sep 05:27
1d36d97

The same maps, faster and in less memory

A performance pass over the whole pipeline with one rule: every output —
GeoJSON, sidecars, tiles — stays byte-for-byte what it was. A NYC subway
chart runs ~16% less CPU; tiling holds one tile in memory instead of a
whole zoom level (half the tiler's peak on a city, far more on a metro
group's z18); the service calendar is parsed once per build instead of
three times; and a stale extract's coverage scan reads the one candidate
the ranking picks instead of every extract in the registry. Retained GTFS
ids no longer pin their whole CSV rows, and a chart child's log is no
longer buffered unbounded in the sync parent.

One deliberate exception to "byte-for-byte": station centroids now sum
their platforms in sorted stop order. They previously summed in Go map
order, which wobbled the last float digit run to run — the same build
could emit two different stations files, and sync's unchanged-tile
detection saw phantom changes. Output is now one canonical byte stream
across runs; coordinates move by at most one ulp against any prior run.

The stop matcher reads abbreviations and forgives rail classes

Two gates in OSM stop matching were dropping stations whose OSM object sat
in plain sight. On the New York subway they cost 29 of 380 stations their
id, W 4 St-Wash Sq among them — eight lines, the busiest interchange in the
system, and the tap that opens it had nothing to open.

The name gate could not read an abbreviation. A feed that shortens every
word and an OSM name that spells every one out share no token at all:
"W 4 St-Wash Sq" against "West 4th Street–Washington Square" scored 0.000
where the 88 m between them demanded 0.126. Only a rare proper noun
survived the difference, which is why Christopher St-Stonewall matched at
99 m while W 4 St failed at 88, and why every casualty was a numbered
street. Tokens now pair one-to-one, exact spellings first and prefixes
after, so a stub never consumes the token its own full form was waiting
for. A prefix pairing is credited the lesser of the two words' weights,
which is what keeps a promiscuous "w" cheap without a rule about stub
length. No word list is involved: abbreviation is prefix truncation in
every language portolan draws.

The class gate demanded the feed and OSM agree on which rail class a line
is. They routinely do not, and neither is wrong — the MTA files the Staten
Island Railway as route_type 2 (regional) and OSM maps it
station=subway (metro), because it is legally a railway and operationally
a subway. All 21 SIR stations went unmatched with their nodes 19-28 m away
under identical names. metro, regional and tram now count as
agreement that it is rail, at a ranking penalty small enough to lose to
real distance but enough that a stop of the station's own class wins
whenever both are in reach. Bus, ferry and aerial stay hard-gated.

Replayed against the real New York extract, misses fall from 29 to 1 — a
station the feed leaves unnamed. Every newly matched pair is its own name
spelled out, at 0-6 m, and the weakest-evidence match in the set
("8 St-NYU" to "8th Street–New York University", 92 m) is correct.

Ids only, for NYC: it keeps "osm_stop_names": false, so no drawn label
moves. Cities that opt into renaming will adopt OSM spellings for stations
that previously had no match.

Requires a tile rebuild — existing pyramids keep the ids they were
built with.

Install

tar -xzf portolan_0.4.11_darwin_arm64.tar.gz
cd portolan_0.4.11_darwin_arm64 && ./portolan help

Each archive holds the portolan binary, the style/ curation
documents it reads by default, and the reference docs. No runtime
dependencies — it is a single static binary.

Verify with sha256sum -c SHA256SUMS.

What's Changed

Full Changelog: v0.4.10...v0.4.11

v0.4.10

Choose a tag to compare

@github-actions github-actions released this 05 Sep 22:32
e141dcf

Clipped extracts keep their geometry

0.4.6 taught a feed whose window reaches past its rail extract to clip that
extract down to the window. The clip decoded each feature into a struct that
embedded feature alongside a second field also tagged "geometry"; Go
resolves that collision in favour of the shallower field, so the embedded one
stayed nil and every clipped feature was written back out with
"geometry": null. The extract carried the right ways and no shape at all.
The chart then found nothing it could draw, and either failed the feed with
"no regular-service rail ways" or wrote an empty collection.

On a global run this took out two thirds of the fleet: 2,746 of 4,262 feeds
charted empty. Only clipped feeds were affected, which is why the large
metros — whose windows already sat inside their extracts — kept drawing
normally and hid the scale of it.

A feed with no covering extract draws what it has

Preflight used to fail a feed outright when its window reached past its rail
extract and nothing else in the registry covered the remainder. That is the
normal case for continental carriers, whose windows are larger than any single
extract in the registry, so the feeds most in need of a railroad were the ones
refused one. It now says so in the log and builds from the extract it has: a
partial railroad beats no map.

The MTA's directions carry their sign names

The direction curation added in 0.4.9 is now filled in for the MTA, whose feed
publishes no directions.txt.

Install

tar -xzf portolan_0.4.10_darwin_arm64.tar.gz
cd portolan_0.4.10_darwin_arm64 && ./portolan help

Each archive holds the portolan binary, the style/ curation
documents it reads by default, and the reference docs. No runtime
dependencies — it is a single static binary.

Verify with sha256sum -c SHA256SUMS.

What's Changed

Full Changelog: v0.4.9...v0.4.10

v0.4.9

Choose a tag to compare

@github-actions github-actions released this 04 Sep 20:13
208681f

Directions can be named the way the signs are

A board reading "Woodlawn" makes a rider work out which way that is; the sign
they are standing under says UPTOWN. GTFS carries direction_id — an opaque
0/1 — and nothing that says what it means. The directions.txt extension
exists and 367 of the 1499 feeds in the fleet publish one, but the MTA does
not, and the vocabulary of those that do is agency-chosen and ragged
(NORTHBOUND, Inbound, Loop, CIRCULATOR, one feed with Inound).

So it is curation, like colour. A style document's agency, route or stop entry
takes a directions map of direction_id to the name on the sign. An agency
naming covers every route it runs and a route naming beats it, so an operator
says Inbound/Outbound once while the MTA lets the L say "8 Av"/"Canarsie"
where the Lexington Avenue lines say "Uptown"/"Downtown". Documents layer per
direction_id, so a city naming direction 0 keeps what the global document said
about direction 1.

The corrected feed now carries directions.txt. A feed with none gains one; a
feed that publishes one has the named rows replaced and every other row and
column kept. A pair nobody named is left out rather than guessed — a board
showing the wrong compass point is worse than one showing none. The console's
Style page edits them alongside colours and names.

Consumers: the corrected GTFS gains a directions.txt wherever a
direction is named. Nothing else in the output moves.

Install

tar -xzf portolan_0.4.9_darwin_arm64.tar.gz
cd portolan_0.4.9_darwin_arm64 && ./portolan help

Each archive holds the portolan binary, the style/ curation
documents it reads by default, and the reference docs. No runtime
dependencies — it is a single static binary.

Verify with sha256sum -c SHA256SUMS.

What's Changed

Full Changelog: v0.4.8...v0.4.9

v0.4.8

Choose a tag to compare

@github-actions github-actions released this 04 Sep 18:26
96b8e2f

A widened feed borrows its extract from the right place

0.4.7 cut Metro-North's rail extract from a national intercity bus
network's, and the build died with no regular-service rail ways. Candidates
were ranked widest-window-first, on the reasoning that one source covering the
whole window beats several covering corners — but the widest window in the
registry belongs to a continental bus operator, which covers everything and
contains no railway at all.

A feed now borrows from a group it is a member of before anything else: that
extract covers every member by construction and carries the same kind of
railway, which no other test here can check. Failing that it takes the
smallest covering extract, since a minimal superset of a feed's own window is
far likelier to be the regional railway it runs on than some continental
network that happens to enclose it.

Install

tar -xzf portolan_0.4.8_darwin_arm64.tar.gz
cd portolan_0.4.8_darwin_arm64 && ./portolan help

Each archive holds the portolan binary, the style/ curation
documents it reads by default, and the reference docs. No runtime
dependencies — it is a single static binary.

Verify with sha256sum -c SHA256SUMS.

What's Changed

Full Changelog: v0.4.7...v0.4.8

v0.4.7

Choose a tag to compare

@github-actions github-actions released this 04 Sep 05:55
897922f

The corrected window now reaches the build

0.4.6 computed each feed's corrected window and then discarded it. A patch run
for Metro-North and LIRR reported registry rewrite: no and skipped both feeds
as clean, leaving them drawn exactly as short as before.

plan.RegistryChanged is only ever set from group diffs, and that flag is what
makes a run write the registry and re-parse it into the config every build
reads
. With it false the run re-read the old file, the widened window never
reached the chart, and the build fingerprint never moved. A widened window now
sets it, and a widened feed joins the changed and affected sets — its clip moved
even though its zip did not, so its own build and any group drawing it are both
stale.

Consumers: 0.4.6 shipped the window fix inert. This is the release that
makes it take effect, so the rebuild it asks for is still owed.

Install

tar -xzf portolan_0.4.7_darwin_arm64.tar.gz
cd portolan_0.4.7_darwin_arm64 && ./portolan help

Each archive holds the portolan binary, the style/ curation
documents it reads by default, and the reference docs. No runtime
dependencies — it is a single static binary.

Verify with sha256sum -c SHA256SUMS.

What's Changed

Full Changelog: v0.4.6...v0.4.7

v0.4.6

Choose a tag to compare

@github-actions github-actions released this 04 Sep 05:40
0a152e2

The global build now covers each feed's own data

A global build was not global. Metro-North came out with 25 of its 114
stations — everything past Yonkers missing — and LIRR with 44 of 127,
clipped well short of Montauk. No error was raised for either.

A feed's bbox is the Overpass window and the shape clip. The eight
original New York entries, authored when portolan drew only New York City,
all carry [-74.26, 40.49, -73.7, 40.92]. For the subway, the ferries, the
tram, the AirTrain and the city buses that window is right. For the two
regional railroads it is not: only 20 of Metro-North's 114 stops fall inside
it. Groups have always taken their window from measured data; nothing did the
same for a plain feed, so a hand-authored window stayed wrong forever and
clipped the railroad away in silence.

A measured feed's window is now grown until it contains the feed's own
shapes. It only ever grows, and only when the window genuinely fails to
contain the data — containment is judged without the margin, since adding
slack first would nudge nearly every feed in the registry by a hair and every
nudge is a rebuild. A widened feed is repointed onto its own
build/<key>-rail.geojson so cutting an extract to the new window cannot
overwrite a fixture other feeds still read, and feedPreflight cuts that
extract from any registry extract that covers the window — clipped, not
merged, because the Northeast Corridor's extract is 166 MB and a feed that
swallowed it whole to reach its own corner would chart as heavily as the whole
corridor. Sync still never calls Overpass. The run logs every window it
corrected; silence is what let this sit.

Consumers: nothing already built changes. A feed whose window was too
small draws more of itself once rebuilt, and its station count rises.

Install

tar -xzf portolan_0.4.6_darwin_arm64.tar.gz
cd portolan_0.4.6_darwin_arm64 && ./portolan help

Each archive holds the portolan binary, the style/ curation
documents it reads by default, and the reference docs. No runtime
dependencies — it is a single static binary.

Verify with sha256sum -c SHA256SUMS.

What's Changed

Full Changelog: v0.4.5...v0.4.6

v0.4.5

Choose a tag to compare

@github-actions github-actions released this 03 Sep 21:23
1068a99

A stop matches the station, not the rails

MatchOSMStops scored candidates on distance and name similarity alone,
with no term for what kind of OSM object a candidate was. A stop_position
— a point on the track where a train halts — carries the station's name and
sits metres from the station node, so the winner was whichever the feed's
coordinate happened to land beside. Clark St, with one station node and two
stop_positions, matched the station; Jay St–MetroTech, with two station
nodes and six stop_positions, matched a stop_position. Downstream that is
the difference between calling a place a subway station and calling it a
subway stopping location.

Assignment now runs in two passes. Station-type objects
(public_transport=station, railway=station|halt, amenity=ferry_terminal,
aerialway=station) are assigned first, and only the feed stations left over
fall back to a stop_position or platform. A fallback rather than a filter:
plenty of stations are mapped with no station node at all, and a
stop_position is a better answer there than none. The second pass stays gated
on radius, class and name similarity like any other candidate, so it cannot
reach further than the first, and each pass still assigns greedily by score,
so two adjacent stations each take their own node.

Consumers who pin station ids: Station.OSMID changes wherever a
stop_position was previously winning, and the adopted station name may change
with it. No drawn geometry moves.

Install

tar -xzf portolan_0.4.5_darwin_arm64.tar.gz
cd portolan_0.4.5_darwin_arm64 && ./portolan help

Each archive holds the portolan binary, the style/ curation
documents it reads by default, and the reference docs. No runtime
dependencies — it is a single static binary.

Verify with sha256sum -c SHA256SUMS.

What's Changed

Full Changelog: v0.4.4...v0.4.5

v0.4.4

Choose a tag to compare

@github-actions github-actions released this 03 Sep 20:06
dc4f7ac

Install

tar -xzf portolan_0.4.4_darwin_arm64.tar.gz
cd portolan_0.4.4_darwin_arm64 && ./portolan help

Each archive holds the portolan binary, the style/ curation
documents it reads by default, and the reference docs. No runtime
dependencies — it is a single static binary.

Verify with sha256sum -c SHA256SUMS.

What's Changed

Full Changelog: v0.4.3...v0.4.4

v0.4.3

Choose a tag to compare

@github-actions github-actions released this 02 Sep 02:06
a518623

The bundling mate range now reaches the bundler

split_merge_dist — the kiss-rule range within which two sustained-parallel
edges are read as one visual corridor — is 16 m, up from 12 m.

It was supposed to move a release ago and did not. The value is written in
three places: a package var in bundle_edges.go, the dial() default beside
it, and pipeline.DefaultDials. The pipeline installs DefaultDials before
every run and dial() prefers any non-zero tuned value, so DefaultDials is
the only one that decides — and it still said 12 while the other two were
moved to 18 and then to 16. Both intermediate values were inert: builds made
with them are byte-identical to 0.4.2, every output file, on both benchmark
networks. All three literals now agree, with a comment at the var block
naming which one wins.

Consumers who pin pixels: geometry moves on every network, because a
wider mate range bundles track pairs that were previously drawn as separate
ribbons. Measured on NYC subway — 1,413 drawn features to 1,443 (+2.1%) with
total ink flat at 2,065 km (+0.03%), so this is corridors being restacked
rather than track appearing or vanishing. It also takes a gate off the
sound score: the sharpest on-corridor turn drops from 51 degrees to 39,
clearing the jaggedness gate, while alignment to the reference network is
unchanged at sub-metre scale (forward mean 2.4 m to 2.5 m, p90 4.7 m to
4.8 m).

The effect is concentrated where tracks genuinely run 12-16 m apart, which
is why this is a patch rather than a redraw: Chicago CTA over the same
range is 1,060 features before and after, with ink flat to five significant
figures at 2,184 km. Expect movement on dense multi-track trunks and almost
none elsewhere.

Uploaded saves are their own group in the atlas

Three things the workbench's .metro upload was missing once there was more
than one of them in the feed picker: uploaded saves now sort under their own
heading rather than among the 1,499 real agencies, they are named from the
uploaded filename rather than the save header (every Subway Builder save is
called "Current Game", so two uploads listed as two identical rows and the
second silently overwrote the first), and they can be deleted —
POST /api/import/metro/delete?feed=<key> removes the config entry and every
file the importer wrote. Deletion is guarded server-side on an explicit
imported marker, not the key prefix, so no curated feed can be removed.

The workbench does not ship in the release archives, so this reaches you only
from a repo checkout.

Install

tar -xzf portolan_0.4.3_darwin_arm64.tar.gz
cd portolan_0.4.3_darwin_arm64 && ./portolan help

Each archive holds the portolan binary, the style/ curation
documents it reads by default, and the reference docs. No runtime
dependencies — it is a single static binary.

Verify with sha256sum -c SHA256SUMS.

What's Changed

Full Changelog: v0.4.2...v0.4.3