The car branch prints the endpoint chain whenever it holds two entries, which
an ordinary rental always does: picked up and returned at the same desk. The
printout then reads "Munich Airport -> Munich Airport", where before there was
no car branch at all and the line simply stayed empty.
Consecutive repeats now collapse and a chain naming one place only is dropped.
A genuine round trip keeps both ends, because Dresden sits between them.
The road trip rail reads `duration_minutes` as a stay, and a place created
without one claimed an hour nobody had entered. The branch answered that by
binding NULL instead, which is right for the rail and wrong for everything
else: the column declares DEFAULT 60, three tests assert it, and the comment
on one of them says so in as many words. That default reaches every place in
TREK, not only the ones on a drive.
So the hour stays, and the road trip solves its own problem where it arises.
A stop added from the corridor search already brings its own figure with it
(RoadtripStopPopup's STOP_KINDS: ten minutes for fuel, twenty for a rest area,
thirty for a charger), and saveStopDraft passes it through, so the kinds that
would be misread as an hour never take the default in the first place.
The road-trip addon's second half: the parts that let a planned drive be
argued with rather than only read.
Reshaping. A via records which stop it follows by position, and that position
is resolved fresh every time the day is routed — so inserting, reordering or
removing a stop moved the ground under every via after it, and the drive
silently snapped back to the road it had been steered away from while the
handle still sat on the map where it was left. The correction is computed
against the day as it stands and written with the same change, in one
transaction: a half-applied re-anchoring mixes two numbering schemes in one
day. The one judgement that needs geometry — which half of a split leg a via
belongs to when a stop lands in the middle of it — is measured on the road
actually driven, so a fuel stop added to a bent leg keeps the bend on the side
it belongs to.
Alternatives. OSRM answers most long legs with exactly one route, which read as
"there is no other way" when a perfectly good slower road existed. When it
offers nothing, the same drive is asked for again without the motorway, and
failing that without the tolls — one question at a time, stopping as soon as
one lands. The offer says what it is rather than how much slower.
Stops along the way. Everything the corridor search can find is a pause, not a
destination: fuel, charging, rest areas, campsites, food and sights all sit
inside the leg they were found on, carry their own colour and icon, and are
left out of every stop count. That colour is defined once and read by the rail,
the corridor list, the map pin and the picker — and it replaced the chains'
logos, which turned a corridor full of petrol stations into a row of
advertisements and left the brands with no logo on file looking like something
else entirely.
How long a stop takes was readable in the rail and writable nowhere; new places
were given an invented hour that nobody had entered. It is now set from the
rail, cleared as a zero because the update statement folds a null into "leave
it alone", and new places start with nothing.
Both columns are rebuilt on one type scale, anchored to the day plan's own
sizes so switching tabs does not change the size of the text beside it. The
rail reads as trip totals, day cards and a chain of stops with the drives
between them; the corridor column as what to look for, how the search is
getting on, and what it found. Six category pills over three lines became one
dropdown, which is the space the results wanted.
Also: the offered routes are framed on the map when the picker opens (`fitKey`
carries no coordinates and each renderer reads it as "the selected day"), road
trip mode does not cluster at all so the whole drive stays readable zoomed out,
and the drive-time labels finally render in the app's own typeface instead of
Leaflet's Helvetica. All of it in both renderers.
The query carries `[timeout:20]`, so the mirror is allowed twenty seconds of
work on it — and the fetch around it was aborted after twelve. A mirror under
load that would have answered at fifteen never got the chance, and the caller
saw a failed area.
That is what makes a corridor search along a long drive report half its
stretches as unsearchable: the stretches were being searched, we just hung up.
Both numbers now come from one constant, the client budget is the query's own
plus room to hand the answer back, and a test holds the relationship rather
than the two literals.
Three read paths built the same nested place object by hand, and two of them
left `stop_type` out of it while their SQL was already selecting the column.
The effect was visible: a petrol station added along a route drew correctly
for a moment from the optimistic write, then the server's answer replaced it
with a place that had no kind, and the road-trip rail redrew it as an ordinary
numbered destination.
`getAssignmentWithPlace` now goes through `formatAssignmentWithPlace` like the
list path already did, which removes the third copy rather than adding the
missing field to it. The two queries in the days service gain the column, and
its hand-built shape gains the field it cannot yet be spared.
The addon shipped with only its two pure modules under test, which left the parts
that actually talk to the network — and the panel a user reads the answer from —
resting on nothing.
Covers the routing hook (one request per travel-mode run, day default over trip
fallback, retry then give up, no re-fetch on a rename, abort on leave), the
corridor search (one query per box carrying every kind, the perpendicular filter,
dedupe, the retry pass before a stretch counts as unsearched), the corridor state
(roads driven over the line between stops, day follow-along) and both panels.
Two findings from writing them, fixed here:
- A dead straight leg is reduced to its two ends by the simplifier, and tiling
walks point to point — so a 250 km motorway stretch produced a box at each end
and nothing in between, while the search still reported itself complete. Steps
longer than half a box are now split before tiling.
- projectOnSegment guarded a degenerate segment with `lenSq === 0`, which two
ends that came through a chain of floating point arithmetic never hit.
Also splits the anchor resolution out of computeSchedule, which was three levels
of nesting for one question, and sorts the POI cache key with a comparator.
The road trip corridor asks for fuel, charging, rest areas and campsites over a
dozen boxes, and asking per category was four times the requests for the same
answer — slow, and four times the chance a shared Overpass mirror times out and
reports the stretch as unsearched.
`category` now takes a comma-separated list. Each OSM selector belongs to exactly
one category, so a hit is still labelled with the kind it answered, and the
result cap is shared out per category so one dense kind can't crowd the rest.
The MCP tool takes an array for the same reason.
Also puts the POI, plugin and route via-point pins on the map's render clock,
the same fix the planned-place pins already had: a library marker repositions on
every pointer sample while the canvas draws once a frame, so the suggestions
swam during a drag and snapped back on release.
A corridor full of petrol stations was a row of identical coloured dots, and the one
thing that tells them apart is the brand. OSM carries brand:wikidata on most of them —
88% of the fuel stations in a Berlin box — Wikidata carries the logo as property P154,
and Commons serves it.
The bytes are proxied and cached rather than linked: one self-hosted instance asking
Wikimedia for a handful of logos is a very different egress profile from every
visitor's browser announcing which petrol stations they are looking at. The cache is in
memory because a GET must not write to the database, and there are only so many fuel
brands; a brand with no logo is remembered too, or every pan would ask again.
A logo-less or unreachable brand answers 204 and the pin keeps its category icon, so
nothing depends on Wikidata being reachable. Both renderers show it, since both read
the same category table.
A petrol station and a campsite both came back as the same nameless grey dot, because
the road categories only existed on the server: the map falls back to grey for a
category it does not know. They now sit in the client's category table too — with the
pump, the bolt, the parking sign and the tent, in their own colours — which both map
renderers and the popup read from the same place.
They stay out of the explore pill's own list on purpose: that pill offers what a city
visit needs, and while road trip mode is on it is not on screen anyway.
The search also stayed quiet when only some boxes failed, which reads as "no fuel on
this stretch" when the truth is that nobody looked. A partial failure now says how many
stretches went unanswered.
The corridor was built from the day's stops, so it followed straight lines across
country the car never drives. Between two cities that line runs through open fields
while the motorway swings wide of it, which is why every hit came back sitting on a
stop rather than on the way.
The days now carry the routed geometry, and the corridor is cut from that. Same trip,
same search: 192 hits spread over the whole 482 km — first fifty, middle, and the last
stretch — where before all of them reported kilometre zero.
Following real roads takes more boxes to cover the same drive, so they go out three at
a time instead of strictly one after another, and the budget rises from ten to sixteen.
The panel head loses the sentence explaining what the sections below already say, and
the day's distance and driving time, which the day list has been showing all along.
Three things the first cut got wrong.
Switching into road trip mode took its time: every leg was its own routing request,
paced a second apart so the shared hosts would answer at all. The router already
returns a leg per consecutive pair of the waypoints it is handed, so a day is now one
request — split only where the travel mode changes, because one request carries one
mode. Answers that come from the cache are no longer paced at all, which is what made
returning to the view feel broken.
A list of petrol stations is half an answer: which one is on your side of the road is
the other half. The corridor search therefore moved out of the panel into the planner,
and its hits are handed to the map like any other place suggestion — grey, because they
are not part of the trip until you add one. The explore pill stands down while the mode
is on: it draws into the same channel and answers the same question for a drive.
The panel itself was a stack of controls without an order. It now reads as header,
search, results: what is being searched (with the day's distance and driving time),
what is being looked for, how far off the route, then the hits grouped by kind with a
count — and a progress bar while the boxes are being worked through.
The road trip mode could show the route but not what sits beside it, which is
half of what planning a drive is about (#1797).
The right column becomes a corridor search: pick what you need, pick how far off
the route you will go for it, and the day's drive is searched box by box. The
boxes come from the route itself rather than the viewport, so the answer is
"on the way", not "on screen"; each hit reports how far off the route it lies
and how far into the drive it comes, and adding one opens the normal place form
so it lands on the day like any other stop.
Overpass is asked in plain bounding boxes rather than with a polyline filter,
because that is what the existing /api/maps/pois route accepts and what the
mirrors can answer; the geometry work is pure and tested. Long routes stop at ten
boxes and say so instead of quietly searching half the drive.
Fuel, charging, rest areas and campsites join the server's POI categories, so the
explore pill and the MCP search_pois tool gain them too. A car booking's stops now
also reach the PDF, which printed pick-up and return with everything in between
missing.
The trip planner could show one day's route at a time and nothing across days,
so a road trip had to be pieced together in Google Maps (#1797, #435).
The addon adds a mode to the plan view rather than a tab of its own: the map
stays the workspace and gains the route over the whole trip, while the left rail
becomes one chain of stops with the driving distance and time between them and
arrival times that follow from them. A time pinned on a stop is kept and restarts
the chain, so a ferry or a museum ticket stays where the user put it.
Car bookings can finally carry the stops the schema has always allowed: the
endpoints table has held role/sequence for every transport type since #384, but
only flights and trains had an editor for them. The MCP tools already accepted
them, so this closes the gap the other way round.
Legs are fetched one at a time and spaced out — the public routing hosts answer a
whole trip's worth of requests with 429 — and the distance cap now looks at the
single leg instead of the sum, which is what dropped long drives entirely.
Two halves of one mistake, both found by the suite.
Letting each side of the merged search catch its own failure meant the index
could contribute nothing while the answer still called itself
"trek-places+openstreetmap". That label is not decoration: the search log writes
it into the corpus a candidate index gets scored against later, so a list that is
entirely OpenStreetMap must not be recorded as though the index had a hand in
it. The source now names what actually contributed.
And maps.service.test.ts drove the index for real. Its fetch stubs answer with
json() and no text(), which is not what the index client reads, so the branch was
entered and then failed on the stub — "uses Nominatim when user has no API key"
was really testing that a stub is incomplete. With the new circuit breaker those
failures also accumulated, and the breaker is process state, so it leaked into
whatever file ran next. trekPlacesSearch is stubbed there now, beside the nearby
stub that was already there for the same reason. The index paths keep their own
files.
Attribution first, because this branch argues the point itself: the card names
the sources with the comment that naming them is a licence obligation rather
than decoration, and the copy beside it promises that every field says where it
came from. The POI path then stamped Overture rows as OpenStreetMap. Overture is
not OpenStreetMap — it carries OSM among other sources under other licences — so
that is a wrong attribution rather than a rounded one. The wire contract keeps
`source` an open string and nothing on the client switches on it, so saying
`trek-places` costs nothing. The MCP tool description no longer calls itself an
OpenStreetMap tool either, and says which source answered.
Adding one place cost two round trips to the index. The details lookup fetches
the place and its answer carries the description; toPlaceRecord dropped that
field, so the enrichment fetched the same place again a moment later to read it.
It is carried through now and read from what the caller already has, with the
second lookup left as the fallback for when there is nothing to read.
That description's source URL becomes an href on the client and was taken on
`typeof === 'string'` alone. It now goes through the same allow-list a place's
website goes through — the file already does this for menu_url, with the comment
explaining why — so anything but http(s) loses the link and keeps the text.
The instance token was hashed from raw APP_URL, which the shipped compose file
leaves commented out. Every default install therefore sent the same token, and a
token every caller shares is not a caller: one instance running a large import
would rate-limit all the others, who would fall back to Nominatim with nothing
to point at. getAppUrl() falls through to ALLOWED_ORIGINS and then to the port,
so unconfigured instances still differ.
Two client-side leaks. A pick made out of the offline cache was recorded into
the shadow corpus, where it moved the two figures the corpus exists to
measure — and offline is a setting, so the network is usually reachable and the
row landed. And the prefetch never re-checked the session after its request came
back: a logout in that window repoints the handle at the anonymous database, so
one person's trip area was written where the next person's offline search reads.
The cache path also ignored the abort signal, so an answer for a keystroke two
keystrokes old could overwrite a newer suggestion list. It rejects the way both
surfaces already know to swallow.
Left alone, and said in a comment rather than hidden: the index cannot localise.
The Overpass path picks name:<lang> and falls back to int_name; the service has
no language parameter at all, so a German reader exploring Tokyo gets the
Japanese primary names on this path.
Three things an install feels rather than reads.
A service that cannot be reached cost the full timeout on every call. Each call
site falls back to the old path, but pays 3.5 s first — so an instance behind a
firewall that drops rather than refuses, or one that simply has no route to the
service, pays it on every keystroke of autocomplete and on all three queries a
booking import runs per venue. Ten venues is over a hundred seconds of waiting
for an answer that was never coming. A process-wide breaker now fails instantly
after four consecutive failures and tries again a minute later, so an
unreachable index costs one timeout instead of one per request. A 404 does not
count against it, because "no such place" is an answer, and neither does an
oversized body, because the service replied.
The offline place cache hung off the map-tile switch. That setting is about
tiles — its own text says "off keeps the cache to trip data + documents only" —
and it gets turned off for space, tens of megabytes of it. A trip's places are
about one. Somebody who turned tiles off therefore got an empty offline search
with no explanation, and "prepare for offline" reported success having stored no
place at all. The two run independently now; the tile progress phase stays gated
on the tile setting, so a run without tiles still reports no tile phase.
Tests for the merged search path, which had none: the wiring around
mergeSearchResults was uncovered, so which list goes in first and what `source`
says were both free to change. Neither is cosmetic — index-first is the whole
point of the feature, and the search log writes `source` into the corpus a
candidate index gets scored against later. Two of the eight cases pin the fix
from the previous commit: an index failure keeps the OpenStreetMap answer, and
neither source is asked twice.
place-shadow lands on its own coverage entry rather than the 80 catch-all, where
a new domain could shed twenty points unnoticed. Pinned under its measured
figure on purpose; the exact ratchet gets regenerated with the repo's own script
after a full run.
trek_places_enabled was read by six call paths — search, autocomplete, the POI
pills, booking-import geocoding, the offline area and details resolution — and
written by nothing. No admin route, no MCP tool, no switch anywhere in the UI,
and the key is not in the settings allow-lists either, so the only way to set it
was an INSERT against app_settings by hand on the server.
That matters more than the four switches beside it, which trade away quota. This
one decides whether every search and every keystroke in the place box leaves the
instance at all. The flag reads fail-open, so an install that upgrades without
visiting the admin panel starts sending them, which is the right default and
exactly why the operator needs somewhere to say no.
Adds the pair symmetrically to place-shadow from the same branch: getTrekPlaces
/updateTrekPlaces on AddonsService, GET/PUT /api/admin/trek-places with an audit
row, and a real switch on the card. The read stays fail-open so it keeps
agreeing with MapsService.trekPlacesEnabled(), and a test pins that agreement
across every stored value — if those two ever diverge the panel shows "off"
while the queries keep leaving, which is worse than either default.
The card said this itself: `available` was a display-only prop whose comment
read "False until the client is wired to the service; the card then says so
instead of offering a switch that would do nothing." It is wired now, so the
prop is a switch. Its two badge strings had been shipped to all 23 locales and
rendered nowhere; they make way for the switch label and a line naming what
answers while the index is off.
Six things the merged search and the offline cache got wrong.
An index failure took the OpenStreetMap answer down with it. Only the OSM half
of the Promise.all had a catch, so a rejection on the index side discarded ten
results that had already arrived and sent the same query to Nominatim a second
time, behind its 1.1 s process-wide throttle. The index refusing a query is
ordinary traffic rather than an outage — a common single word without
coordinates is turned down upstream as too expensive — so this fired in normal
use. Each side now catches its own failure, and the keyless fallback reuses the
answer OpenStreetMap already gave instead of asking again for a list that
cannot have changed.
Dedup never fired for a non-Latin name. namesOverlap splits on [^a-z0-9], so
長谷寺, Кремль and مسجد الشيخ زايد all tokenise to nothing and the function
returned false — the same place came back once from each source, one row under
the other, and picking either gave it a different id. That is the corpus this
whole feature was measured on. Two names with no Latin words are now compared
strictly, folded through NFKC so the width variants the sources disagree on
still match; sharing a character is deliberately not enough, because deduping
東京タワー against 東京タワー駅前郵便局 would swallow a real result.
expand=1 answered nothing for an index place while expand=0 answered in full.
The expanded lookup degrades OSM ids to the plain one and had no branch for the
new family, so the richer request was the poorer one, over REST and MCP alike.
The bias circle could exceed Google's 50 km ceiling. A trip spread over more
than that produced a radius Google rejects outright, turning a search into an
error toast. Clamped on both sides — the client because it draws the box, the
server because the radius arrives over the wire.
The offline cache was rebuilt on every sync and never deleted. syncTrip wrote
its meta row with put(), which replaced the whole record and dropped the
fingerprint saying the cached area was current, so every login, reconnect and
manual sync re-downloaded megabytes that had not changed. And clearTripData
never touched areaPlaces, so a trip switched off for offline use kept answering
offline searches and its rows stayed for the life of the install.
clearCachedPlaces, which claimed to do that job, was called from nowhere; its
only test asserted with .every() on an empty table and passed on nothing.
Picking an offline suggestion ended in an error toast. The details lookup had
no cache path, so it failed and the callers fell back to searching for
"name, address" — which the cache matches on the folded name alone, so it found
nothing for a place that was sitting right there. A miss stays a miss.
Four things the index changed and nothing followed:
`gers:` was missing from NON_GOOGLE_PLACE_ID, so isGooglePlaceId said true
for every id the index hands out. Five call sites gate on that function to
keep unresolvable ids away from Google, which answers them with a billable
400 INVALID_ARGUMENT: photo refs, the editorial summary, the place-photo
route, the expanded details lookup and the enrichment photo pass. Since the
index answers first for search and autocomplete, a GERS id is what an
ordinary new place now carries, so this was the normal case rather than an
edge one — three billed lookups per added place, none of which could ever
return anything.
The area download shared the search path's megabyte cap and its 3.5 s
timeout. Both are right for a keystroke and wrong for the offline prefetch,
which asks for 3000 rows at once: a city-sized answer sits on that megabyte,
readCapped threw, and the caller could only report "index unavailable" — the
trip then went offline with an empty place cache and nothing said so. The cap
now follows the row count the caller asked for, with a floor, and the bulk
call gets a budget that fits a bulk download. It is still capped, so a broken
upstream cannot hand us an endless body.
searchPlaces returned before the Google branch as soon as the index or
OpenStreetMap had anything, and both set google_place_id to null. That is
right for a search and wrong for the one caller that wants an identity rather
than an answer: list-import enrichment (#886) exists to attach a
google_place_id to a bare imported pin, and pickEnrichmentMatch discards every
candidate without one. So the import silently stayed unenriched on an instance
that pays for a key. That caller now asks for the Google identity explicitly.
Nothing changes for an instance without a key — enrichment could never resolve
anything there, before or after.
Tests cover the three functions this branch had left untested, which is what
put the maps ratchet under its functions threshold: the autocomplete index
path (a new file, MAPS-AUTO-001..007), the gers details branch when Overpass
is down (MAPS-GERS-008), and the client's abort timeout — including that the
signal exists at all, which nothing asserted before.
The coverage ratchet measures per domain, and this branch put new code into
four domains pinned well above the floor without tests reaching it.
addons (99/99/99): getPlaceShadow and updatePlaceShadow had none at all, and
the parity the service comment claims with PlaceShadowService.enabled() is now
asserted rather than asserted in prose.
maps (93/86/97/96): geocodeQuery was invisible - its only consumer stubs it
away - so the new suite drives the gate, an accepted hit, a hit with an
unusable coordinate, an index failure falling through, and both ends of the
Nominatim tail. It sits on the booking-import path, where it decides whether
thirty sequential lookups leave the instance.
admin (86/70/89/88) and place-enrichment (96/95/94/97): the place-shadow
handlers and their audit entry, the getAppConfig read, and nearbyWouldMislead,
whose lines ran incidentally while nothing asserted the result - the guard
could have been inverted and the suite would have stayed green.
toPlaceRecord wrote the brand's Wikidata item into `wikidata`, and
getPlaceDetails spreads the record over the OSM details, so for a chain branch
picked from the index that value replaced the building's own tag - and nulled a
real one when the place had no brand. resolveIdentity reads that field as the
place's own identity, so the photo ladder returned the chain's Commons files as
pictures of this branch and the description came back as the company's article
without the aboutBrand flag.
That is precisely the failure WIKI_IDENTITY_TAGS and readBrandIdentity were
written to prevent, in their own words: "a chain's logo passed off as a photo of
the restaurant would be a plain lie". The item now travels under brand:wikidata,
the key OSM uses and readBrandIdentity already reads, and resolveIdentity picks
it up from the payload for a place OSM does not know - so a branch still gets
the chain's description, flagged as such, and never its logo.
The client test also covers trekPlacesArea, the streaming half of readCapped
including the size cap, the configured base URL and the nearby defaults, none
of which any test reached.
The index-first branch hardcoded opening_hours: null while the payload it maps
carries them, and search_pois advertises "opening hours wherever OSM carries
them". It also capped the radius at 20km and then answered clamped: false, so a
wide viewport came back narrowed and said it was not - the same field the
Overpass path fills honestly.
The three red MCP cases were the same code telling the truth from the other
side: they stub Overpass one level below the facade, so pois() ran for real,
and because trekPlacesEnabled() fails open on a database with no row, a unit
test was sending live requests to places.liketrek.com from CI. That is what
answered with 50 places instead of the stubbed one. The index client is now
mocked in that suite, with the gate left open so the cases still run on the
shipping configuration, and the index-first path gets cases of its own here and
in a new maps.pois suite: field mapping, the empty and the failing index both
dropping through to Overpass, and an outage under both providers still reaching
the caller as an error rather than an empty list.
maps.service.test.ts drove pois() with no stub at all and leaked the same way.
When details fails to resolve a picked suggestion the sheet falls back to a
text search (#1192). That call was the one search left in the sheet without a
location hint, so the fallback could land on an identically named place in
another country - exactly what the hint exists to prevent, on the path taken
when the first hop already failed.
useLocationBias reads useTripStore; the sheet's harness only ever built a
planner prop. So the hook saw an empty store, sent no hint, and the one case
that asserts a box went red while its three siblings passed vacuously - they
expect undefined, which an empty store produces no matter what they seed.
capturedBias now seeds places, assignments and selectedDayId the way the
running app has them, which puts all four cases back to work. The 500km case
is retargeted from Tokyo/Sapporo to Tokyo/Nagoya: 264km across, biased under
the old diagonal rule and dropped under the 60km radius one, so it guards the
rule that actually shipped. A new 029b pins the day-first precedence - a trip
too wide to bias still hints at the day being planned.
The three AdminPage cases located the card with closest('.bg-white'), which
returned null once the card became bg-surface-card, and within(null) reports
that a line later as "container is null". Anchor on .rounded-xl, the same way
the green AdminSettingsTab suite already does, and ask for the single Test
button rather than the first of several: weather left the card when it stopped
needing a key.
FE-ADMSET-031 asserted the Open-Meteo panel inside that card. The panel was
deliberately removed from the desktop tab - it needs no key and had nothing to
configure - so the case now pins what took its place, the TREK index heading
the card with the recommendation the Google key used to carry. The panel itself
still lives in the mobile section, where FE-MOB-ASET-019 asserts it.
MapsSearchDto validates a locationBias and MapsService passes it through, but
no caller ever sent one. That gap is the whole difference between the numbers
in this PR and what the software does. Measured against a live instance over
the 127 places of a real trip, searching each by name:
without a hint 50.0% -> 54.8% top-5
with a hint 50.0% -> 73.0% top-5 (this PR reports 72.7%)
mess/eval_merge.mjs sends lat/lng on every index call and a viewbox to
Nominatim; the client sent neither. Without coordinates the index refuses
common queries as too expensive and contributes to 41% of searches instead of
100%, so the merge degenerates to Nominatim alone.
The hint has to be NARROW, which is the part that is easy to get wrong. Same
corpus, same instance, varying only how the box is derived:
no hint 54.8% top-5, 38.1% at rank 1
box around the whole trip 57.1% top-5, 17.5% at rank 1
box around the nearest few places 70.6% top-5, 34.9% at rank 1
A box spanning a round trip has its centre in open country between the cities.
It points at nothing and costs more than half the rank-1 hits. useLocationBias
therefore prefers the day being planned, falls back to the trip only while it
still fits inside one region, and drops the hint past a 60km radius rather
than sending a misleading one.
This also replaces the two hand-rolled copies of the bounding box in the
planner dialog and the phone sheet, which shared a defect: Number(null) is 0,
so a place without coordinates entered the box as the Gulf of Guinea and
stretched it across the planet.
Measured on 128 places out of a real trip to Japan, all saved through
the old search. Index alone: 51.6 percent inside the top five. Index and
OpenStreetMap alternating: 72.7 percent.
Two cleverer orderings were tried first and both lost. A score threshold
cannot separate the cases at all: at 0.75 it catches 19 of 62 misses and
throws away 6 of 66 hits. Promoting results whose name matches what was
typed is worse than it sounds, because "Hase Station" shares a word with
"Hase-dera" while OpenStreetMap answers in the local language and
returns the right temple under a name that shares nothing: that rule put
seven weak index matches ahead of the correct answer, which landed at
position eight. Alternating puts it at position two.
So the merge does not rank. Each source keeps its own ordering and they
take turns, index first. An exact business match still opens the list;
a landmark Overture does not carry is second rather than eleventh.
Measured against 128 places out of a real trip to Japan, all saved
through the old search: the index answered 51.6 percent inside its top
five. For most of the rest it returned the shops AROUND the landmark
rather than nothing, which is worse than nothing, because ten plausible
wrong answers look like an answer.
Overture Places is a dataset of businesses and is very good at those. It
largely does not carry temples, bridges, riverside walks, viewpoints or
observation decks, which is a fair share of what somebody planning a
trip searches for.
Two ways to detect "the index does not have this one" were measured and
both failed. Its own score does not separate the cases: a 0.75 threshold
catches 19 of 62 misses and throws away 6 of 66 hits. Name overlap does
not either: 57 false positives, because "Hase Station" shares a word
with "Hase-dera".
So this does not classify. Explicit search asks both concurrently and
merges, name matches first and the index ahead of OpenStreetMap within
each group. Autocomplete is untouched, because Nominatim's policy names
it as unacceptable use. Nominatim also gets the location bias it never
had, without which "Hase-dera" returns the temple in Nara rather than
the one in Kamakura the user is standing beside.
Two booking-import cases still stubbed searchNominatim after geocoding
moved to geocodeQuery, and stubbed it with the old array shape, which
made "has coordinates" pass on an undefined lat.
The index can now carry opening hours, from two sources: an
OpenStreetMap layer the service keeps as a separate database file, and
the schema.org data a place publishes on its own site, read in the same
page fetch the description comes from.
OSM stays the first choice where it has them. Measured across seven
countries it covers 27.5 percent of gastronomy, and an OSM entry
describes this exact object and gets corrected by people who walked past
it, where a chain's website often carries one set of hours for every
branch. The rest is what the operator publishes, which measured 8.3
percent on the same kind of place.
Both go through the same expansion into weekday lines, so a card reading
`opening_hours` gets one shape and not two.
Five maps.service cases went red when search started answering from the
index ahead of Google; they are about the shape of the Google request,
so they now switch the index off first.
An offline instance had the trip's own places and nothing else, so "find
a pharmacy near the hotel" had nothing to answer from: every lookup went
to a server.
The index can hand over a bounding box in one request, so it does, once,
next to the map tiles. A trip to Rostock is roughly a megabyte, refreshed
only when the trip's area actually moves. Search and autocomplete fall
back to it at the API layer, which is why all ten call sites got the
offline path without any of them learning about it, and the answer says
`source: 'offline-cache'` rather than pretending to be live.
Country packages stay what they were: a SQLite file for anyone who would
rather run their own copy of the service. A browser cannot query one
without shipping a WASM SQLite, and Germany alone would not fit in
IndexedDB, so that door stays where it belongs. /v1/packages lists them,
because "you can leave" only means something if the door is visible.
Overture has no descriptions, and the encyclopaedias only describe the
places that have an article, which is a fraction of a percent. What
Overture does have is the address of the page the description is on, for
43 percent of places, and those pages publish a summary in JSON-LD or
og:description precisely so machines can read it.
The API fetches each page once and keeps the summary; every instance
reads it from there. TREK never opens a business's website itself, which
would mean one request per instance per place against small servers.
In the ladder it sits below the encyclopaedias and above Google: an
article about this exact place beats the operator's own copy, but for a
restaurant or a shop the operator's summary is the only one that exists.
Credited by host and linked back, with no licence claimed.
The corpus behind "is our own index good enough to replace Nominatim".
When somebody searches and then picks a result, the client posts the query,
the rank and the pick. Nothing else: no user, no trip, no session, and
coordinates only to three decimals.
Deliberately decoupled from the search endpoint. Correlating a response
with a later pick would mean handing out a ticket on POST /api/maps/search
and threading it through all fifteen call sites; the client already holds
everything the row needs at the moment of the pick.
Evaluation happens offline afterwards: export the corpus, run it against a
candidate index, count how often the pick came back in the top five. That
is why collection can start before such an index exists, which is the whole
point, because the corpus needs weeks of real use and the index needs a
server, and those two waits should not be serial.
Off unless an admin switches it on, and it reads === 'true' rather than
!== 'false'. The three older places switches fail open because a migration
backfilled a row for installs already using them; nothing uses this one, so
an instance that upgrades and never visits the admin panel logs nothing.
Reading is admin only, retention is 180 days, and the switch state is on the
unauthenticated app-config endpoint on purpose: whether an instance records
what its users search for is something a visitor may know before logging in.
The card was a flat list: two key fields, four Google switches and a large
weather panel, all at the same level. That gave a switch you set once the
same weight as the key it depends on, and gave the loudest treatment on the
page to the one thing with nothing to configure.
Now three comparable provider blocks. The TREK index carries the accent and
the recommendation, because a key should be the exception rather than the
expected setup. Google carries no border badge, so it does not stand out,
and says plainly what it costs beyond money. Weather has left the card: it
needs no key and had nothing to set.
The "recommended" badge moved from Google to the index, which is the whole
point of the change.
Also here, and both are their own thing:
- The settings tab is two columns from xl up. Nearly every card is "label
left, switch right", so the middle stayed empty while the page scrolled
for ages. Explicit columns rather than a grid over the flat list, because
a grid pairs cards row by row and leaves a hole under the shorter one,
and these differ by hundreds of pixels.
- Pictures: the nearby fallback drops from 300m to 60m and is skipped
entirely for shops and gastronomy. Measured across 600 places in six
cities, 2 percent of ordinary businesses have a picture on Wikimedia
against 70 percent of churches, so for a cafe the curated rungs never
fire and the fallback always does. That is how the town hall ended up
over the doner shop. It fails open on an unknown category, so landmarks
keep the fallback that works for them.
vite.config reads the dev port and the API target from the environment,
defaulting to what they were. Several worktrees could not run side by side.
Adds a provider for the project's own place index and puts it in front of
the existing ones for search, autocomplete, details and the explore pills.
It needs no key, has no quota, and what it returns may be stored, which is
true of neither of the two providers it sits in front of.
Nothing about the Google path changes. Every call site falls through to
exactly what it did before when the index has no answer or the service is
unreachable, so a working search keeps working.
Two details worth knowing while reading:
- Details for a place from the index also resolve its OpenStreetMap record
by name and coordinate, because the index has no opening hours yet and
never has cuisine or a menu link. Without that the hours a user saw while
adding a place vanished from its card afterwards.
- The explore pills keep reporting source 'openstreetmap'. It is a typed
contract the client switches on, and the places are the same places.
TREK_PLACES_URL points at a different instance; unset means the public one.
Transitous has no GTFS coverage for much of Asia, so a search between two
Osaka subway stops finds nothing — the stops are not in the feed at all.
Add Google as an optional backend behind an admin setting, reusing the
install's existing Places key, and fall back to Transitous whenever no
key resolves.
Uses the Routes API rather than the legacy Directions endpoint the
request named: every other Google call here already speaks the
v1/X-Goog-Api-Key family, and Google closed the legacy endpoint to Cloud
projects created after March 2025.
Google returns walking as turn-by-turn navigation steps where MOTIS
returns one leg per walking segment, so consecutive walk steps are
coalesced into a single leg — otherwise a finely-sliced walk pushed a
real journey past the 20-leg cap and the whole itinerary was dropped.
Both responses now name the backend that answered. The fallback is
silent by design, which left an empty result ambiguous between "no data
here" and "the provider you picked never ran", and the admin panel warns
when Google is selected but no key resolves — or when only the admin's
own key is set, which serves them and nobody else.
Cost shaped the implementation, since Google bills where Transitous does
not: both field masks are cut to the cheapest SKU tier that still
answers the question, cache TTLs are longer than the Transitous path's,
and route alternatives ride along inside the already-billed request.
Addresses #1699
A probe map is exactly the value someone reaches for when their cluster is
unhappy, so the two ways of reaching for it should not break the render.
`probes: null` hit a nil pointer on `.Values.probes.liveness`; guarding with
`with (default dict ...)` also makes dropping a single probe work, which is what
you want on a pod whose liveness check is the thing killing it.
The rest is documentation the chart already gives every other value: a wiki
section next to Resource Limits, a Notes bullet, and a values comment naming the
one trap here, that a handler switch has to clear the shipped httpGet in the same
override or the API refuses the pod.
Verified with helm template: defaults render byte for byte what the chart
produced before, probes=null renders no probes, probes.liveness=null leaves one,
and helm lint passes.
The version window already said 4.0.0 to 5.0.0, and the test above it is even
called "covers the whole 4.x line", but that was only half the story: without
`recurring: 'per-version'` a notice falls back to a one-time dismissal, so
everyone who closed the modal on 4.0.0 never saw it again. It did not come back
on 4.1, 4.2 or 4.2.1, which is the opposite of what the window says.
The mechanism was already there and in use one entry below, on thank-you-support.
The dismissal path records the app version it was closed at, so the notice
re-surfaces on the next upgrade and stays down within the same version. Rows
dismissed before that column existed read as 0.0.0 and simply come back once.
A grid item defaults to min-width:auto, so the two date cells never went below
the width of their contents: a long locale date plus the keyboard button beside
it pushed each cell past its track, and on a phone the end date came to sit on
top of the start date with its own button clipped off the edge. Measured on a
412px viewport, the row overflowed its container by 30px.
Both cells get min-w-0 so the track wins, and the picker's own trigger gets it
too, which is what finally lets the ellipsis it already carries do its job
rather than the button simply growing. "12. Sept. 2026" now reads "12. Sept…"
in a narrow column instead of escaping it.
Keeping the locale format: the report suggests ISO 8601, but the date is
formatted from the user's language everywhere in the app, and the overlap is
the layout, not the wording.
Two reports, both about an import that fails without saying why.
#2262 — temperature: 0 sat in the shared request body, so it went out on every
attempt including both retries. Reasoning models (the gpt-5 family) reject any
explicit temperature, and the json_object retry fires on any 400, so the import
spent two doomed requests and could not succeed at all. The parameter is now
dropped once a server has explicitly said it will not take it.
Not as a third link in the chain, though. A gpt-5 rejects max_tokens AND
temperature, the API names one parameter per response, and it may name either
first — a chain of one-shot ifs converges on only one of the two orders. The
retries are a loop over what the server actually said instead: each remedy
applies at most once and they compose. Detection is a phrase, not the bare word,
because dropping temperature costs the deterministic sampling that small local
models depend on, and an unfamiliar wording still falls through to the
json_object retry exactly as before. NuExtract and Ollama are untouched: the
first attempt always carries temperature, and it is the only one they ever see.
#2261 — the bundled extractor is Debian trixie's 24.12.3, roughly eighteen
months behind upstream, and the reporter is right that this is invisible: a
stale extractor, a mistyped KITINERARY_EXTRACTOR_PATH and a genuinely
unsupported provider all look the same from the UI, which is nothing imported
and no reason given. The version now appears in the startup log and in a new
admin-only GET /api/admin/system-info, and LOG_LEVEL=debug passes the
extractor's raw stderr through — the JS ERROR and Invalid-result-type lines the
filter drops are the only evidence that a vendor script is missing or failing.
Capped at 200 lines, and opt-in because those lines can carry document
fragments.
Not bumping the package: the filter the reporter needs first ships in KDE Gear
26.08 and no Debian suite carries it, and sid's 26.04.3 wants KF6 6.22 and Qt6
6.10.2 against trixie's 6.6 and 6.8.2 — a partial upgrade of Qt6, KF6 and libc6
from unstable into the production image, on two architectures, that still would
not fix the reported carrier. It comes for free with the base-image move after
Debian 14.
Two suites encoded the behaviour this branch changes: MReservationSheet
asserted the literal metadata: null the forms no longer send, and the new
Your-share case reached for a label that is a card footer rather than a
heading.
Five findings from auditing the release branch, none of them a blocker for it,
all of them things it would have shipped with.
The mirrored booking price only survived an edit when the form happened to fill
metadata of its own. Both reservation forms and the phone sheet sent
`metadata: null` whenever the form had no metadata key to send, and null clears
the column outright — so restaurant, event, tour, parking, other, a hotel with
no check-in times and a transport with no carrier details all lost the price
again on the next save. They send the object now: it still clears what the form
dropped, and the server carries the price across (#2233).
Your share counted expenses nobody has paid for, while the balances right below
it stopped counting them. The tile contradicted the panel it sits in (#2225).
App.tsx kept its own list of routes that must not mount authed-only widgets, and
it had already drifted from the one the response interceptor uses: this one
named /public/journey/, that one covers all of /public/. It reads
isAuthPublicPath now, so a route cannot be public to one and not the other.
LLM_TIMEOUT_MS and TREK_PLUGINS_IGNORE_TREK_RANGE shipped in .env.example but
not in the compose file most installs actually read.
And a typo in the Russian offline copy: Завершёна → Завершена.
The credential strip and the 303/302 body downgrade are new behaviour in a
security helper, and src/utils sits on a 96% line ratchet that the untested
branches took under.