Skip to content

Vela - feature list

Status legend: โœ… done ยท ๐ŸŸก partial / in progress ยท โฌœ planned

At a glance (jump to a section for the detail). For how each of these works and the code behind it, see SPEC.md.

Area The short version
Map & rendering Keyless OpenFreeMap/Protomaps vector tiles, Google-style POI markers, hillshade, in-app light/dark, scale bar
Search & POIs Live keyless Google search - name/rating/reviews/hours/price/website/photos/popular-times/"people also search for"
Routing & traffic OSRM turn-by-turn (primary) + Google traffic ETA & jam reroute; alternates; offline on-device routing (135-region world catalog)
Navigation Maneuver banner with a real lane diagram + highway shields, spoken + haptic guidance, speedometer, re-center, arrival summary; Android Auto (first cut)
Location AOSP LocationManager + rotation-vector heading - no GMS/Fused
Offline Downloadable basemap tiles + OSM POI index + address geocoder (typed address โ†’ route, no signal) + whole-state place packs (Organic-Maps-style offline search) + routing graphs + open building-footprint overlay (Microsoft, ODbL); combined map+routing area download; quiet offline indicator (no banner)
Platform GrapheneOS/no-GMS, CI-signed nightly + weekly-stable v0.4.<run> releases, Obtainium + own F-Droid repo, in-app updater
Resilience Signed remote calibration (pb/paths/JS) + notices - hot-fix drift without an app update

Map & rendering

  • โœ… AMOLED true-black map & navigation theme (2026-09-14). The AMOLED theme now extends from Settings to the map canvas and navigation overlays. applyAmoled layers a pure-black (#000000) land, water, dark-gray road network, black building footprints and black text halos on top of applyDark (ensuring boundaries, rail, aeroways, and untouched layers keep dark styling). The map styleKey includes |amoled= so changing theme modes reloads the style immediately. Navigation overlays (NavControls, NavSearchChips, StepsSheet, SpeedWidget, and the current road label pill) use SheetPalette.bg(dark, amoled) with subtle dark borders (SheetPalette.BorderAmoled), while the maneuver banner preserves its teal container accent for instructional clarity and the route line remains standard traffic-coded blue.
  • ๐ŸŸก Offline regions are moving to the obf format (issue #214, in progress). The routing engine, download store, bake pipeline and catalog cutover are built and device-verified end to end (a phone in airplane mode downloaded a Berlin obf, routed a 21 km drive with named steps and road shields, and deleted the region cleanly); baking the world's regions is the remaining step. A region download shrinks to roughly a quarter of the GraphHopper-era size (Berlin: 105 MB graph to a 27 MB routing section), avoid options work offline again, and walking/cycling route offline for the first time. Regions installed before the cutover keep routing until re-downloaded.
  • โœ… One glyph per intersection for stop signs, lights and cameras (2026-07-25). OSM maps street furniture per approach, so a four-way stop arrived as four stop signs and a signalized junction as up to eight lights; they now merge to one icon at the junction centroid, like Google. Flock camera multi-head corners merge to a single badge below street zoom and split back into individual cameras with facing cones when zoomed in; the route camera counts stay per-head. Fewer always-drawn symbols is also a small render win.
  • โœ… Fixed speed/radar cameras on the map (issue #229, 2026-08-03). Settings, Map, "Speed cameras": draws OSM-mapped fixed enforcement cameras as amber badges from neighborhood zoom, keyless via Overpass with the same mirror failover, area caching and stream parsing as the other road-furniture layers. Off by default; fixed installations only, since mobile speed traps have no keyless data source. Coverage follows OpenStreetMap, strongest in Europe.
  • โœ… Speeding alert (issue #404, 2026-09-14). Settings, Navigation, "Speeding alert": off by default. Once you have been over the posted limit for a few seconds Vela says "You're over the speed limit", once; it speaks again only after you have slowed back under the limit for a while and crept over it again. Uses the same posted limit and the same tolerance as the speed badge, so it fires exactly when the badge turns red, and only where a limit is known.
  • โœ… Plate camera alerts while navigating (2026-09-16). Settings, Map: "Plate camera heads-up" shows a card and "Say when a plate camera is ahead" says "License plate camera ahead" as you approach a Flock / ALPR camera on your route. Both are off by default and separate from each other and from the map layer. Same timing as the speed-camera warning (about twelve seconds of notice, never for a camera already behind you, quiet when you are stopped), once per camera per route; cameras within 40 m of each other along the route are one alert ("License plate cameras ahead"). Uses the bundled camera data, so no network. The voice follows the spoken-directions mute.
  • โœ… Google routes your trips with stops (2026-09-21). A trip with stops used to be routed through them by the open router alone, with Google's direct origin-to-destination time painted on as a speed ratio; Google is now asked for the trip through the stops, so its traffic chooses the roads and its time is the one shown, and the open router names the turns along Google's line. The route chooser checks that Google's line really passes every stop before trusting it.
  • โœ… Side streets around plate cameras (2026-09-21, issue #600). Settings, Navigation, Cameras: "Try side streets around cameras", nested under "Avoid surveillance cameras", off by default. When routes still pass cameras, the chooser tries a point just off the road at each camera cluster (up to three per route, nearest first) and routes through it; a route that passes fewer cameras inside the same detour limit (the lesser of 25% or 10 minutes) goes to the top of the list with its camera badge. Since 2026-09-28 every camera-bearing route gets the pass, not only the one that leads, under one shared budget: Google routes and prices each candidate with traffic; at most six extra route requests per trip, four on any one route, and a corner already tried from another route is not tried again. Starting a drive on that route keeps the detour through reroutes and rechecks (the detour points ride along as silent stops: never spoken, never listed). A mid-drive edit of the stops keeps the detour: the silent points still ahead are merged back into the edited list in route order.
  • โœ… Plate cameras are counted by the way they face (2026-09-16). A plate reader sees traffic moving along its road, not across it, so a camera with a known facing only counts for a route when it points within 50 degrees of that route's direction (either way) at the nearest stretch. Cameras with no facing still count. This applies to the per-route camera counts, the "Avoid surveillance cameras" pick, the route bar marks and the new alerts; the map layer still draws every camera.
  • โœ… Vela gives memory back when the system asks, and adapts to small phones (2026-07-23, adopted from the vela-dpad fork). The speech model (about a quarter gigabyte while loaded) now unloads after two minutes unused and reloads in about a second on the next mic tap; map caches, the hidden web fetchers, and the image cache all release under real memory pressure instead of forcing the system to kill the app. Low-memory devices additionally skip speculative warm-ups and fetch a leaner map POI set, which targets exactly the out-of-memory crashes reported on budget tablets.
  • โœ… Voice search says why it failed, and a corrupt speech model can't crash-loop the app (2026-07-23, adopted from the vela-dpad fork). Every voice-search failure now names its cause (model missing, mic permission, recorder busy, and so on) in a dialog with a retry, instead of the listening sheet silently closing. A corrupt or truncated engine download is quarantined after two failed loads and only that engine's files are removed; re-downloading clears the quarantine. Voice providers with no label now show their app name in the picker instead of a blank row.
  • โœ… Sheet chrome and landscape fixes (2026-07-23). Swiping the handle of a place with nothing to expand no longer makes the search bar and layers button vanish: they only hide when the sheet measurably covers them. Rotating the phone with a place card or results list open now resizes it to the new orientation instead of keeping the old height over the search bar, and the landscape side panel widens to half the screen (400 to 520dp) instead of a fixed 400dp.
  • โœ… Street-level buildings preload while you idle (2026-07-23). Zooming down to the ~200 ft view used to wait a beat while the building footprints streamed in. When the map sits still for a moment at a mid zoom, Vela now quietly fetches the buildings for the area you're looking at in the background (disk cache, no extra memory), so the zoom-in paints instantly. Canceled the moment you move the map; never runs during navigation.
  • โœ… Settings redesigned hub-and-spoke (2026-07-23, ported from the vela-dpad fork by alltechdev). The single very long Settings page is now a short hub of category rows, each opening its own small sub-screen (Appearance / Map / Place pages / Navigation / Voice / Search / Offline maps / Saved places / Data & privacy / Diagnostics / About) - far less scrolling on any screen, and a keypad phone reaches any section in a few presses. Comes with an AMOLED black theme option (true-black surfaces) and a settings search box on the hub: type a setting's name and the match opens the right sub-screen directly. Every mainline setting survived the port (the fork's version had dropped Material You, map colors, interface size, POI sizing, parking history, lists export/import, the nightly toggle and more - all restored into the new structure). Device-verified on the Pixel 4a: hub, search-to-spoke, Appearance/Navigation/Voice/About walks.
  • โœ… Street View from a search result shows the mini map, not the results list (2026-07-18, user). Opening the viewer clears the selected place, and the results sheet - which wins the bottom slot whenever nothing else claims it - fell through and drew the POI list over the bottom-half mini map. The sheet is now gated while the viewer is up (or loading) and returns when it closes. Device-verified on the exact repro path (search, open a result, Street View).
  • โœ… Closing a place from the results list leaves the camera where you are (2026-07-18, user). The map used to forget its cluster framing while a place sheet was open and re-fit ALL results on close - a zoom-out yank away from the place you were just looking at. The fit is now remembered across a place sheet (and Street View), so closing returns to the list with the camera untouched, which is also exactly what the Google app does. The deliberate re-frame still fires in its intended case: pan away, collapse the list, pull it back up.
  • โœ… Collapsing the results list no longer snaps the camera to your location (2026-07-18, user). The camera's generic branch fell back to the live GPS fix as a target whenever nothing else claimed it, so merely minimizing the results sheet (an inset change) flew the map away from the results to wherever the user physically stood, with a zoom snap on top. The live fix is now only a fallback target on an EMPTY map - results/markers up means collapsing the sheet leaves the camera exactly where it was.
  • โœ… Offline indicator stops crying wolf (2026-07-18, user). The offline flag latched the instant the connectivity callback saw no active network - but WiFi-to-cellular handoffs and doze wakes routinely pass through exactly that moment, so the app flashed "Offline" (and gated fetches) while genuinely online. Going offline now requires the condition to hold for ~3 s; coming back online applies instantly; and any completed live fetch (search or the ambient fan-out) heals a stale flag on the spot. A search no longer latches offline at all - it only heals, and its own failure path still shows the offline guidance when the network is truly down.
  • โœ… The Vela voice is labeled and one tap away (2026-07-18, user). The fleet-default navigation voice (HFC Female today, remote-calibratable) now reads "Vela voice (HFC Female)" in the voice browser, and whenever it is not installed - a failed download, a crash mid-install, cleared storage - the browser opens with an "Install the Vela voice" button at the top instead of making the user hunt the English group for the starred row.
  • โœ… Locate button no longer rubber-bands (2026-07-18, user). Tapping current-location flew to the fix and settled, then the next GPS fix (meters away) re-triggered the generic camera branch at a slightly different default zoom - a second flight that read as a lurch or snap back. A jitter gate now treats a live-fix target that moved under ~40 m as the same place (adopted silently, never flown to), and every browse fly (locate, launch center, search, recenter) lands at the same 15.5 (~1000 ft) - the tap was the lone 15.0 straggler, which is why the follow-up flight visibly changed zoom. Category searches ("restaurants") used to return exactly one page of 20, ranked by Google's keyless web ordering, which is prominence-heavy over the whole visible box - so a modest restaurant the user was standing next to ranked 21+ and never listed. Two fixes: (1) a FULL first page now triggers pages 2 and 3 concurrently (the same !8i offset pagination the web map uses, verified live: each page returns 20 fresh places), one extra round trip for up to 60 results; specific-name queries return partial pages and never paginate, costing nothing. (2) matching places from the ambient pool (the tight-span category fan-out that feeds the map dots, which usually already holds the neighbor) are APPENDED to the results, nearest first, deduped against Google's list - never reshuffled into it, since the old distance re-rank experiment proved that floats junk above landmarks. Zero extra network for the merge. Some budget tablets' GL drivers (seen on a Samsung tablet's Android 14 update with a Unisoc chip) kill the app the instant the map's GL surface initializes - before the user can reach any setting. Vela now marks "map initializing" at surface creation and clears it on the first finished render; two consecutive launches dying inside that window automatically switch the map to TextureView rendering (a different graphics path that avoids most of these driver bugs, slightly slower) and the app just works from then on. Zero configuration. Since 2026-09-03 only deaths the OS recorded as a NATIVE crash count (Android 11+, ApplicationExitInfo): a force-stop, a swipe from Recents, a low-memory kill or a Java crash elsewhere no longer count, because two of those had flipped a healthy phone into the slower path for good without anyone noticing. When the sentinel does engage, the setting's row says so with the date, and Settings โ†’ Developer โ†’ Compatibility rendering is the manual override in both directions. Known-fragile chips skip even the two crashes: Unisoc devices ON ANDROID 14 (the reported tablet's family + OS, identified from the hardware string; the documented driver faults are specific to the Android 14 builds) default straight into compatibility rendering on their first launch. The same silicon on older Android renders fine the normal way (car head units run this chip on Android 10 without issue) and is deliberately left alone, covered by the crash sentinel like everything else. Verified on-device: two simulated init deaths flip the fallback on, the map renders fully through it, and the toggle restores normal rendering; a Pixel stays on the normal path untouched.
  • โœ… Browsed areas keep their Google-quality places for weeks, online or offline (2026-07-17, user). The ambient POI layer's disk cache grew into a durable store: the newest 32 browsed areas (up to 200 places each, about 1 MB total) persist for 14 days, paint instantly on a cold launch, and are served whenever Google cannot answer - an airplane-mode cold start in a town you browsed yesterday shows its full POI layer (device-verified). Write-through: being online keeps the store current for free (every live fetch overwrites its area) and caching never skips a fetch, so nothing gets staler than your last visit. The place sheet's details fetch stays the live truth for hours, phone and closed status, and a business it finds permanently closed is written back so its dot stays gone across restarts (and a just-closed sheet's pin never resurrects a dead place). Offline can no longer poison the store: a no-network fetch comes back as an empty SUCCESS (each category request swallows its error), and that empty pool used to blank the painted dots and overwrite the cached area with nothing - empty pools are now treated as no-answer (keep what is painted, serve the freshest covering area regardless of age, retry on the next settle), and blank areas are dropped at load. Groundwork for warming a whole city into the store at offline-download time (planned follow-up).
  • โœ… Big map-performance batch, worst-case-device tuned (2026-07-17, user). A day of profiling on a Pixel 4a (the deliberate low-end reference) landed a set of coordinated changes: (1) Buildings draw Google-style by zoom tier - flat footprints from z16 (~250 ft), 3D extrusions from z17 growing to full height by z19, with sides shaded (vertical gradient) so same-colored faces read as discrete buildings; dense-city towers no longer bury the roads at 500 ft, and the most fragment-expensive layer starts a zoom later. (2) The Microsoft building-footprint overlay auto-hides where OSM already has the area. A bounded probe (12-point coverage sample, at most one run per 1.2 s, only at z16+) reveals the overlay only where OSM is genuinely sparse and only after a finished render, so it never flashes over a city while tiles stream in; layers are created hidden and earn their reveal. Coverage is measured by area, not feature count (tile generalization merges a dense block into a couple of giant polygons, which a count misreads as empty). (3) Settings โ†’ Advanced โ†’ Fill missing buildings turns the overlay off entirely, and Settings โ†’ Developer โ†’ Building overlay debug shows a live badge (overlay state + a UI-thread fps readout) for testing. (4) POI density follows zoom like Google (a tighter on-screen cap when zoomed out, the full cap when close) and the mid-stream repaint batching escalates once the map reads full, so dense arrivals re-run symbol placement ~2-3 times instead of ~8. (5) Search lands at ~1000 ft (z15.5, same as the locate fly-to) instead of z16.5, so searching a dense city arrives on roads+labels+POIs with the whole building stack one pinch away - a London search on the 4a settles at ~59 fps. (6) Flock/ALPR cameras hide below z13 while browsing (they draw with overlap forced, so a zoomed-out metro was a wall of badges); a planned route keeps the low floor so route overview still shows its cameras. (7) Street View pauses the ambient fan-out - panning the mini-map no longer scrapes/parses/repaints POIs behind the sphere. (8) The place you just closed is pinned into the next POI repaints for a couple of minutes, so the icon you tapped can't vanish to ranking jitter when the sheet closes. All verified on-device across a dense downtown, a college town, a small plains town and a big-city search, plus the standing suburb cases. used to blank every ambient icon around it (the layer was gated to the bare map) and closing the sheet re-placed the whole layer, then a refetch a beat later reshuffled it again. Now the area's icons stay up around the opened place, Google-style (only the opened place's own copy hides under the red pin), and a fetch newer than 3 minutes that still covers the view is served as-is instead of re-asking Google for the same area (its ranking jitters between identical requests, which is what randomly swapped or dropped icons after closing a sheet).
  • โœ… Compass moved clear of the layers button (2026-07-15). The layers circle sat exactly on the compass slot in the top-right corner; with the layers button enabled the compass now sits below its touch target.
  • โœ… One-line landscape chrome, Google-style (2026-07-15, user). In landscape the browse map's search bar takes half the width and the category chips scroll beside it on the same line, so the top-right stack (layers button, compass) rises a whole row; the stacked portrait layout pushed the compass down into the parking and locate buttons on a phone's ~390dp landscape height. Focusing the bar grows it to full width in place (the field must not remount or focus bounces) and the search page renders full-width below as usual. Portrait is unchanged, and the tiny keypad phones the D-pad work targets are portrait devices, so they never see this layout.
  • โœ… Landscape side panel for results and places (2026-07-20, user). In landscape the search results list and the place sheet render as a width-capped LEFT panel (Google's landscape treatment) instead of a full-width bottom sheet, so the map keeps a real strip beside them and every right-side button (layers, parking, my location) simply stays visible, no situational hiding rules. The camera inset moves to the left axis to match: a focused pin or result cluster frames in the strip beside the panel. The minimized place card keeps its name, rating and action pills (Directions/Call) in landscape too, via a dp floor on the minimized detent (0.26 of a ~390dp landscape height only fit the name row). The layers button's overlap gate short-circuits in landscape (the panel never reaches its corner); in portrait the locate button now rides above a minimized place card's measured top edge instead of hiding. Scale bar yields to the panel; the satellite attribution centers in the map strip. The panel's EXPANDED detent caps below the search bar (landscape is a two-stop ladder, minimized <-> tall): the bar deliberately stays in landscape, so the portrait hide-the-bar-when-expanded rule is portrait-only and the panel stops under it instead of sliding beneath a bar that keeps taking taps.
  • โœ… Chinese, Chinese (Taiwan) and Japanese (2026-07-11, issue #55). Vela speaks 14 languages now. All three layers: UI chrome (values-zh Simplified, values-zh-rTW Traditional with Taiwan wording, values-ja), generated turn-by-turn text (three new NavStrings tables; Chinese splits by script so Taiwan gets ่ฟด่ฝ‰/ๅ…ฌๅฐบ/ๅœ“็’ฐ), localized place content (hl=zh-CN / zh-TW / ja with open/closed keyword tables for both scripts), Whisper dictation (multilingual model, pinned per language), and a Mandarin neural voice (Huayan) in the voice library that pairs with both Chinese scripts. Japanese has no Piper voice yet, so ja guidance speaks through any installed system Japanese TTS (the standard fallback); see ROADMAP.
  • โœ… Two map color sets, remote-defaultable (2026-07-11). Settings โ†’ Appearance โ†’ Map colors: Modern (the Google-app pixel-sampled palette, the default) or Classic (Vela's archived earlier look: white roads, yellow motorways, true greens). The fleet default rides the signed calibration channel (defaultMapPalette), so it can change without an app release; an explicit pick always wins. Classic's night palette re-differentiated (2026-07-12): Modern's dark was re-sampled to Google's deep navy, which left Classic's old blue-gray dark reading almost identical (user: "classic looks bluish like modern, houses too"). Classic dark is now a NEUTRAL charcoal-slate identity - slate land, GRAY buildings (no blue houses), muted-amber motorways echoing the classic-light yellow, its true greens and steel-blue water kept - so the two dark sets are clearly distinct. Follow-up (2026-07-12): the Microsoft building-footprint OVERLAY (the layer that fills OSM's gaps) had its color hardcoded to Modern, so in Classic those houses stayed Google-navy while the OSM ones went gray; it now honors the palette too. Also, since the two sets currently diverge mostly in dark mode, the Settings hint says so.
  • โœ… Nav puck: the position is filtered too (2026-09-01). The puck's speed had been Kalman filtered since June; its position never was, so every meter of along-route GPS noise was a meter the arrow actually traveled, once a second, all drive - and no amount of smoothing downstream could stop it, only soften it. AlongRouteFilter folds each fix in weighted by the fix's own reported accuracy, so a clean fix pulls hard and an urban-canyon one barely moves the puck; progress advances in the rate domain, so it can neither stall nor lurch. Route geometry is now requested from OSRM at polyline6 as well, since the default encoding quantizes every vertex to a ~1.1 m grid and made straight roads arrive kinked. Simulated end to end: along-road wobble 7.2 โ†’ 2.8 px at cruise, 27 โ†’ 5.5 px in a canyon, stalled frames 2.7-28% โ†’ 0%.
  • โœ… Share a recorded trip without sharing your front door (2026-09-01). Share on a trip now opens a trim dialog instead of exporting the raw CSV. Share trimmed (the default button) deletes every fix within 200 / 400 / 800 m of the start, the end, the recorded destination and your Home and Work, keeping the middle of the drive at full precision, plus the five other places the same fact hides: the trip's name (trips are named after where they went), the destination in the file header, the spoken lines at either end ("Arrive at โ€ฆ"), the maneuvers in the trimmed zones, and the start of the saved route line. Timestamps are rebased to zero so the file does not say when you drive. Unknown line kinds are dropped rather than passed through, so a tag added to the format later cannot be published by a scrubber written before it existed. The dialog shows what was removed and the first surviving coordinate before anything leaves the device; Share full trace is still there, and the on-device copy is never modified. Sharing several trips at once trims every one of them the same way. core/replay/TripScrub, 15 tests.
  • โœ… Several trips share as one zip, with one trim for the lot (2026-09-16). Select trips โ†’ Share now sends a single vela-trips-<date-time>.zip instead of separate attachments, which did not arrive in every messenger (Signal dropped them). A dialog picks one trim distance for the whole batch and says, before anything is built, how many trips made it in, how many points came off and how many are left out for being shorter than the trim. Each trip in the zip is trimmed exactly like a single share; one that trims to nothing is left out, never sent raw. The saved-trip list is easier to scan too: each row is the drive's date and time over its distance, duration and name, with Replay and Share buttons and Rename and Delete in a menu. Redact places in exports is now always shown in Settings > Diagnostics and also starts every trip share on the largest trim distance (the middle of a drive is still never rounded). core/replay/TripShareBatch, TripShareBatchTest.
  • โœ… Car navigation icon (2026-09-27, discussion #611). Settings > Navigation > Navigation icon: the arrow, a car in red, blue, white, green or yellow, a UFO, a pirate ship or a rubber duck, with a preview. Since 2026-09-29 the four alternatives are 3D models in navigation: they stand on the tilted map and turn with the heading instead of lying flat.
  • โœ… Minimized route chooser (2026-09-28, issue #616). Tap the handle (or swipe down, or pan the map) to shrink the chooser to one line with the time and Start, so the route shows on small screens; tap again to bring it back.
  • โœ… Sea and ocean names in your language (2026-09-28, issue #619), list pins as large as place icons (#618), and the road name in the bottom bar no longer clipped (#617).
  • โœ… Walking directions from OpenStreetMap (2026-09-28, issue #478). Walks come from the open foot router. Google's walk is offered only when it is at least 15% shorter, with its turns named from the map's own street data instead of being forced onto OSM, which added detours. The same naming is the fallback when a picked Google driving route cannot be matched.
  • โœ… Offline maps on an SD card (2026-09-28, issue #613). Settings > Offline maps > "Store downloaded regions on": internal storage or the SD card, moving what is already downloaded (routing, places, map, place packs, overlays, glyphs, saved areas).
  • โœ… North-up navigation by default (2026-09-27, issue #612). Settings > Navigation "Start drives north-up" starts every drive flat with north at the top; the compass still switches it during a drive, and re-centers when the map was panned away.
  • โœ… Place page Overview tab (2026-09-27). Tabs sit under the action buttons and pin while scrolling; Overview holds the info rows, popular times, highlights, a review summary card, About and related places. Reviews load only when their tab opens. The Reviews tab has sort and search chips and the star breakdown again; a Photos tab shows the gallery as a grid with category chips. Options to load photos only on tap and to skip the popular-times retries. Business updates (the owner's posts) get their own Updates tab, with the newest on Overview, from the reply Vela already fetches.
  • โœ… Reviews and route traffic on tap (2026-09-27). Settings > Places "Load reviews only when I tap" shows a Show reviews button instead of loading them; Settings > Privacy "Live traffic only when I tap" plans routes on the open router alone until Show traffic is tapped on the route list.
  • โœ… The route picker says when a route was computed offline (2026-09-12, issue #350). A route from a downloaded region shows "offline route, no live traffic" where an online route shows its traffic word, so you always know which kind you are looking at. Downloaded regions remain the fallback: with signal, routes still come with Google's live traffic.
  • โœ… On foot or by bike the arrow lets go of the route sooner (2026-09-12). The arrow used to stay glued to the planned line until you were 22 m off it, the car setting. Walking and cycling now use 8 m plus a share of the GPS accuracy, so cutting a corner across a crosswalk shows you where you are within a fix or two.
  • โœ… Custom list order (2026-09-12, issue #343). Your lists can be put in any order: up and down arrows on each row in the Your lists dialog (keypad reachable), and the order sticks everywhere lists appear, on the search page and as pins on the map.
  • โœ… Arrow size and colors (2026-09-12, issue #344). Settings > Navigation: the navigation arrow comes in Normal, Large (1.25x) and Extra large (1.5x), and in a white-disc-with-blue-arrow variant for people who found the blue disc blending into the blue route line. One setting drives both the follow-mode arrow and the map symbol.
  • โœ… Google-style nav puck (2026-07-11). The navigation arrow is a white chevron inside a filled bright-navy circle with a soft drop shadow (no white ring, per user feedback); it rotates about the exact GPS point. Replaces the bare blue chevron. Enlarged twice from feedback (2026-07-11/12): 96 โ†’ 112 โ†’ 136 px so it reads clearly at a glance while driving.
  • โœ… Bold street names (2026-07-11). Road-name labels render in the Bold font stack in both themes, like Google; other labels keep regular weight.
  • โœ… Bike paths in Google's teal (2026-07-11). Dedicated cycleways (OSM highway=cycleway) draw as their own teal line layer, split out of the green foot-trail network. On-street painted lanes are not in the keyless tile schema (Overpass follow-up in ROADMAP).
  • โœ… "1 result" is finally singular (2026-07-11, issue #56). The results-count bar uses real Android plurals in all locales, with the correct plural categories per language (Russian, Polish and Ukrainian get their one/few/many forms).
  • โœ… Result filters grew up (2026-07-10). The rating and price chips open real menus (rating tiers 3.5+/4.0+/4.5+ like Google, price levels $ through $$$$) and Sort is a menu of Relevance / Rating / Distance, instead of chips that blindly cycled through hidden states. New "Wheelchair accessible" filter - the one place attribute Google's response carries per result keyless (parsed off the language-neutral attribute id, so it works in all supported languages); deeper attribute facets (vegetarian, reservations, โ€ฆ) only exist in the per-place About data, so they can't filter a result list keyless. Cuisine facets are deliberately absent: in a search-first app the query is the cuisine filter, and Vela's filters are local to the fetched results rather than server re-queries.
  • โœ… Privacy comparison matrix in the README (2026-07-11). A plain-words table under Privacy comparing the Google Maps app, Google Maps web, and Vela: account linkage, device identifiers, GPS exposure, map-browsing visibility, search anonymity, routing telemetry, saved-place storage, ad profiling, and offline capability. PRIVACY.md stays the per-endpoint deep dive; the table is the ten-second version.

  • โœ… Urgent pushed notices can be a modal (2026-07-10). A calibration.json notice with level "urgent" renders as a dialog (OK + optional Learn-more link) instead of a map card, for announcements that must be seen - the "servers overloaded, hang tight" class of message. Same signed channel, same one-time dismissal.

  • โœ… Minimizing the place card fades gracefully (2026-07-10). The photos and body content fade out with the height as the sheet glides down instead of vanishing in one frame at the swap, and the compact card (name, stars, action pills) fades in - nothing pops. The header circles also became fixed-size plain buttons: the Material button's minimum-touch-target machinery kept re-inflating their layout past the visible circle, which is why they overlapped through two rounds of shrinking.
  • โœ… The photo viewer goes truly full screen (2026-07-10). Google's treatment: system bars hide while viewing (swipe reveals them), the backdrop is the photo itself blurred and dimmed instead of flat black (Android 12+), and the author/date stamp sits bottom-left in every orientation. The full-screen reviews page also draws edge to edge so rotation can't leak the map through the borders. The photo strip's category chips are gone (the Menu tab replaced them), and the header's circled buttons slimmed to 36dp with wider gaps so they never touch.
  • โœ… Restaurant menus get their own tab (2026-07-10). A place with menu photos shows a Menu tab beside Reviews and About: a browsable photo grid, titled with Google's own localized gallery-tab name, tapping through to the full-screen viewer. Google's keyless data carries no menu LINK (probed: the search payload has none), so photos are the menu surface; making the gallery scrape menu-exhaustive is the known follow-up for places where the tagged set is partial. The inline review search also folds into a magnifier beside the All-reviews pill - the stacked text field read as clutter, and the full-screen panel's server-side search stays the headline way to dig.
  • โœ… Gallery survives rotation + the save menu (2026-07-10). The full-screen photo viewer draws edge to edge, so rotating to landscape no longer lets the map peek through the system-bar strips, and the photo caption's bottom clearance scales with the screen instead of floating to the middle in landscape. The place header's star is now the whole save menu (quick save, save to a list, edit note, set as home/work), replacing the separate overflow button that crowded the circled header.
  • โœ… Place sheet modernization (2026-07-10). The header's Save/Share/more/close buttons sit in Google's subtle gray circles, every pill on the sheet shares the app's stadium shape, the reviews summary is Google's block (big number, stars and count stacked beside it, left-aligned) instead of a centered strip, the "All reviews" button drops the dated outlined look for the sheet's tonal pill language, and the minimized card carries Call and Website beside Directions (same gating as the full sheet) so it's useful without re-expanding.
  • โœ… One house number per address (2026-07-10). The open-address overlay used to print an apartment complex's number over its whole footprint (OpenAddresses has a row per unit/parcel); the overlay build now collapses those to a single point per address, so each number appears once. Takes effect per state as the overlays rebuild.
  • โœ… The full-screen viewers match Google's chrome (2026-07-11). The photo viewer and the full reviews page render in the app's own full-screen window (not a child dialog), so they truly reach every edge and survive rotation without the image floating on a shrunken background. The status bar stays visible over a gradient at the top, Google-style. The reviews page's back arrow became an X (matching the photo viewer, on the left) and swipes down from the top to close.

  • โœ… The photo viewer and reviews page reach the true screen edges (2026-07-10). A window dump showed compose dialogs are wrap-content windows measured against inset bounds, so they stopped about a status bar short of the display at each end no matter which window flags were set; their roots now demand the real display size, which pulls the window out to the edges.

  • โœ… The Menu photos stop including things that were never the menu (2026-07-10). The gallery scrape used to tag whatever was on screen while a category tab was selected, page chrome and the previous grid's leftover tiles included. It now snapshots the screen before each tab click and tags only what appears after the switch, dwells more than twice as long on menu tabs so long menus are walked to the end, and recognizes the menu tab's name in all supported app languages.

  • โœ… Settings reorganized + navigation UI refresh (2026-07-08, user request). Settings had grown disjointed, so the sections now follow how you actually use the app: Appearance, then Map (traffic, transit lines, 3D buildings), then a new Place pages section (Show reviews, the "Read all reviews" button, Load photos), then Navigation, then Voice right after it (voice serves navigation), then Offline and the rest. The four stacked vibrate-on-turns switches became one row of chips, one per travel mode. And the in-drive UI got a facelift: the maneuver banner and the bottom bar are now soft-cornered floating cards with real shadows, a bigger turn glyph, a bolder distance, a heavier road name, and filled tonal buttons instead of thin outlined circles. Verified in a simulated drive.

  • โœ… Arrive step names the destination (2026-07-08). The ARRIVE step in the maneuver banner, the step list and the arrival card shows the business name and its address under the "Arrive at your destination" line, Google-style. Handles partial data (offline routing often has no business name): name, else the address, else the raw coordinates - and no line ever just repeats another (an address search's "name" IS its address, so it prints once). NavSession.destinationDisplay (pure, unit-tested DestinationDisplayTest); the address rides NavSession.State.destinationAddress โ†’ navDestAddress.
  • โœ… Zoomed-in pan smoothness (2026-07-08). Three targeted trims for the "stutters when zoomed right in" report: (1) the scale bar no longer recomposes every pan frame - the per-frame camera listener only pushes a new value when it changes over 1%, which is when the drawn bar could actually differ; (2) house numbers (both the basemap layer and the streamed overlay) no longer occupy the symbol collision index (textIgnorePlacement) - they still yield to icons and labels, but the densest symbols at street zoom now cost the placement pass almost nothing, and a number can never evict a business icon regardless of layer order; (3) a new Settings โ†’ Map โ†’ 3D buildings toggle (on by default) controls the z16+ fill-extrusion layer - extrusion is the most fragment-expensive thing the map draws, so turning it off is the one-tap fix on weaker GPUs; the flat footprints stay either way. Localized.
  • โœ… Highlight transit lines (train + subway, 2026-07-07, user request). Settings โ†’ Map โ†’ Highlight transit lines draws rail on the map in color, Google-transit-layer style: heavy rail in purple, subway/light-rail/tram in teal. The data is already in the keyless OpenFreeMap basemap tiles (OpenMapTiles transportation source-layer, class = rail / transit), so this is just a colored LineLayer over the existing tiles, no new data source, no network (ensureTransit in VelaMapView, width zoom-interpolated, inserted below the first symbol layer so station/road labels stay on top). ON by default (2026-07-07, user call - rail is useful to see and the data's already in the tiles at no cost), persisted (TransitLayer holder). Device-verified: a metro's light-rail line draws in teal downtown. Removes cleanly when off; no-op on a non-OpenMapTiles basemap (a MapTiler variant / the demo style). Localized in all supported languages. (Station markers are a possible follow-up; this ships the lines.)
  • โœ… Terrain shading is a toggle, OFF by default (2026-07-12, user request). Google doesn't shade topography unless you ask, and the hillshade muddied Vela's clean flat basemap, so Settings โ†’ Map โ†’ Terrain shading now controls it and it starts off. It was never baked into the tiles - the relief is a live HillshadeLayer over the keyless terrarium DEM - so this just flips the layer's visibility (Topography holder, ensureTopography in VelaMapView, mirrors the transit toggle); off = the DEM tiles never fetch. Flip it on for terrain context.
  • โœ… Reviews search circle no longer clips (2026-07-12, user report). The magnifier button beside "All reviews" was an IconButton with a 44dp background circle; IconButton forces its own smaller box, so the circle overflowed and clipped on the right against the sheet edge. Now a plain sized, clipped Box draws the circle cleanly.
  • โœ… Build a route between two points on the map ("Choose on map", 2026-07-07, user request). You could always route from your GPS location to a tapped place, but not pick an arbitrary START (or stop). The origin/stop picker now has a Choose on map row: tapping it leaves the search overlay and shows the live map with a center crosshair, a hint banner, and a Set start / Set stop button (Google's "Choose on map"). Move the map under the crosshair and confirm, or long-press anywhere to set the point directly (both work). The picked point is reverse-geocoded (falls back to a bare pin) and the route recomputes. So a route between two arbitrary points is: long-press a destination, open Directions, tap From, Choose on map, set the start. MapPick enum + pickOnMap state; chooseOriginOnMap/chooseStopOnMap/confirmMapPick/cancelChooseOnMap in the VM; onMapLongPress honors the mode; the directions panel / place sheet / top search bar hide while the crosshair is up, and system-back cancels it. Localized in all supported languages.
  • โœ… Camera stays put when you close a place sheet or exit Settings (2026-07-07, reported bug). Two separate causes. (1) Closing a place sheet dropped the map's bottom inset to 0, which nulled lastCameraTarget in VelaMapView and let the fall-through camera branch re-center on the now-stale state.center (the tapped place) at the zoomed-out browse level, so the map jumped back to the place and zoomed out after you'd panned away. The inset handler now only re-frames when the sheet APPEARS or grows, never when it closes. (2) VelaRoot swapped MapScreen out for SettingsScreen, disposing the remembered MapLibre MapView; returning rebuilt the map from scratch and re-seeded it from the stale center at the default zoom. Settings now draws as an opaque overlay ON TOP of a still-composed MapScreen, so the map view (and its camera/zoom) survives the round trip. Follow-on fix (2026-07-07): because closing a place no longer moves the camera, the ambient POI dots (loaded for the previous center, and opening a place pans the camera to it) could end up off-screen with nothing to reload them. clearSelection now refreshes the ambient POIs for the current view, so they come back when you close a place.
  • โœ… Snaps to you on launch, even after a crash relaunch (2026-07-12, reported bug). A cold launch centers on your location once GPS resolves, but a crash relaunch came back zoomed to the whole US - the process restart wipes the in-memory center, and MapLibre restored its last (wide) camera before the first fix landed, and the gentle center = center ?: here didn't override it. Now a view-layer one-shot centers on you the moment the map AND the first fix are both ready (it waits for the fix, however long GPS takes) - running in the view, after MapLibre has restored its camera, so it reliably overrides the stale wide view. Skipped once you've taken the wheel (a pan, or a search/route already owns the camera). One-shot per session. (An earlier VM-side gate couldn't work: center is seeded from the last-known fix at init, so it was never null.)
  • โœ… MapLibre Native vector rendering (Compose-wrapped)
  • โœ… Detailed open basemap: bundled OpenFreeMap Liberty + injected house numbers at close zoom; OpenMapTiles vector source pinned to OpenFreeMap's versioned tile path (the un-versioned path serves empty tiles - that was a blank-map bug)
  • โœ… Route line, tappable Google-style search-result pins, location dot as GeoJSON layers
  • โœ… Heading-up, tilted navigation camera; fit-route-to-screen on preview; recenter FAB. Pinch-zoom during nav keeps the follow-camera (2026-06-21): a pinch keeps tracking your position at the zoom you chose (a dedicated OnScaleListener adopts it as an override and suppresses the auto-zoom while you pinch), while a pan still detaches to let you look around (OnMoveListener) and Re-center clears the override back to auto-zoom + follow - telling the two gestures apart, which the move-started reason alone couldn't. (Fixed 2026-06-21: the override snapped back to auto a beat later because a navFollowing-keyed reset fired every time follow re-attached; now the zoom is captured continuously during the pinch and cleared only on a real pan or at nav-end (keyed on navMode). And the pan-detach moved from onMoveBegin to onMove - onMoveBegin could fire before onScaleBegin, so a pinch was misread as a pan and detached the camera the instant you zoomed; onMove runs after scaling is set, so a pinch now locks your zoom AND keeps tracking (Google-style) and only a real pan detaches.) Predictive framing - tried + REVERTED (2026-06-21): aiming the camera a little ahead of the puck (to see into turns) inched/stuttered on real GPS even off the smooth progressM, and feel-wise the plain smooth puck-follow was what worked - so the camera follows navPuck.drawn directly. (See-into-turns can return later via in-frame padding that lowers the puck WITHOUT swinging the camera target around.) De-jittered 2026-06-19 (was re-animating every recomposition โ†’ lag/shimmer; throttled to real movement >4 m / turn >2ยฐ with a 550 ms ease). Per-frame follow (2026-06-21): that throttle still felt stiff (the camera re-pointed only ~1โ€“3ร—/s); the follow-camera now runs in the motion ticker at 60 fps, easing toward the smoothed puck point each frame (~0.12 s) for a continuous glide - seeded from the live camera on (re)attach so the hand-off from the pre-engage framing / a Re-center is smooth. The old throttled block is kept only for the pre-engage / off-route case. (Feel change - confirm on a real drive; revertible.) With speed-adaptive zoom (Google-style: pulls back on the freeway to see ahead, tightens on city streets - continuous + low-passed since 2026-06-21: hard speed-band thresholds made the zoom ping-pong ("zooms in and back") whenever speed hovered near a boundary in stop-and-go, so it's a smoothly interpolated zoom over a damped speed now). 2026-06-21: during nav it now follows the smoothed motion-model puck point (see the location-indicator entry below) rather than the raw GPS fix, so the view can't lurch to a far spot when the snap is briefly ambiguous
  • โœ… Traversed route grays out behind the vehicle (Google-style) - a line-progress gradient on the route (gray for the part driven, live traffic ahead). During nav the route line carries traffic per segment, not the whole map (2026-06-19 per feedback - the /maps/vt whole-map overlay no longer auto-shows when navigating; it washed every road, and the ask was traffic on "just the road we're on"). The route line is now colored per segment like Google - free-flow blue with amber/red bands over the congested stretches (from route[3][5][0] in the directions response; see "Routing & traffic"). The manual traffic toggle still paints the full map when you want it. Nav-overlay polish: speedometer + recenter lifted clear of the bottom bar, recenter is now an icon-only FAB tucked bottom-right. Maneuver-banner swipe fixed - pointerInput(Unit) had captured a stale step index so every swipe repeated one card; rememberUpdatedState makes each swipe walk to the next/prev step
  • โœ… Compass kept clear of the status bar (inset-aware margins)
  • โœ… Tap a labeled POI or a search-result pin to open it; camera frames all results after a search. Tapping a POI also reads name:latin/name:en (not just name), and an unnamed POI icon (an apartment gym, an unnamed park/playground) reverse-geocodes to a pin + address instead of being a dead tap. When several Google listings share the same spot (e.g. a co-branded "SpeeDee Midas" with a sparse Midas and a rich SpeeDee profile), the tap now opens the most-reviewed = canonical one rather than whichever happens to be a few feet nearer
  • โœ… Bottom sheets (place sheet, steps) fill to the screen edge - content is padded off the gesture/nav bar, but the sheet background no longer stops short and lets the map peek through at the very bottom
  • โœ… House numbers in Germany and the Netherlands (issue #257 round two, 2026-09-22). They were hidden by an Alaska bounding box that crossed the antimeridian and so "covered" every point between 49.8 N and 73 N, which switched the map to the (empty there) address overlay. Fixed in the live manifests, the catalogs, the bake scripts and the region-cover rule.
  • โœ… Tapped places load like Google's (2026-09-22). Tapping a place opens its sheet with the name at once and pulsing placeholders for the photos, rating, details and body while the listing is looked up; when it lands, the details fade in and the sheet stays put instead of re-opening. If the lookup hangs, the map's own data shows after a few seconds. What the map's own data already knows shows immediately (category, address, phone, website, and hours where OpenStreetMap or a chain's store locator has them, including from downloaded place packs), and Google's listing replaces it in place. The lookup itself is about three times faster: it asks for one page of results instead of three.
  • โœ… "Where is this place from?" (2026-09-22). A tapped map place that is not yet (or never becomes) a Google listing says so in small gray text under its name: "From Overture", "From AllThePlaces (chain)" or "From OpenStreetMap", then "checking Google" or "not matched on Google". An OpenStreetMap row's line opens its node, so a wrong place can be fixed at the source.
  • โœ… Taps check the house number (2026-09-22). When a tapped place and a Google listing both have a street number and they differ, the listing is not linked, so a station can no longer open the one across the intersection.
  • โœ… Open-data hours read like Google's and compute open or closed (2026-09-22). OpenStreetMap and store-locator hours are converted for about 98% of real listings (it was 86%), including comma-separated days, holidays and sunrise-to-sunset parks, so the sheet shows the week and says "Open" or "Closed" even with Google off. The places data also keeps OpenStreetMap's hours when another source already had the same shop, and a chain's store-locator hours now survive the same way. Open-data addresses carry the city, state and ZIP ("โ€ฆ , Davis, CA 95616") instead of the street line alone.
  • โœ… Fuel stations (and fire, police, charging stations) link again (2026-09-22). Their open data category contains "station", which sent the lookup down the transit-stop path, and it never linked. And a fuel row named for its site rather than its brand now finds the gas listing beside the building its name points at.
  • โœ… Tap a house number or a building to open its address (2026-07-08, user request). A single tap on a house-number label (the map's own addr:housenumber or the streamed address overlay) opens a pin + place sheet snapped to that exact number - tapping a numbered label opens exactly that house number, not a neighbor's. This matters because Google's reverse-geocode snaps to the nearest addressable point and routinely returns a different house (device: the raw geocode of a tapped label came back a few doors off); so the tap LEADS with the number on the label and uses the geocode only for the street/city. A tap on a plain building footprint (no number showing) reverse-geocodes the building to its address the same way. A real business on that spot still opens as the business. Empty land has no footprint, so a tap there does nothing and only a long-press drops a raw pin (below). Device-verified in a residential suburb (a tapped number opened exactly that house; a bare footprint reverse-geocoded to its own address)
  • โœ… Long-press the map โ†’ drop a pin, reverse-geocode it to an address (Nominatim/OSM, keyless), then get Directions - works even where no building is drawn. When the point doesn't snap to a street address (a bare road, open land, or a failed geocode) the sheet surfaces the lat/lng coordinates prominently (a MyLocation-iconed row, tappable to copy) beside the road name we already show - Google-style. A house-numbered snap ("1020 Olive Dr") or a real business POI shows its address instead, so no clutter; the snap is detected by the name's first token being a pure-digit house number (a numbered street like "120th St" keeps its "th", so it reads as unsnapped โ†’ coordinates)
  • โœ… Keyless OpenFreeMap Liberty basemap (active, loaded by URL - the setup that renders on-device, no key): Google-style POI markers + category-colored labels, a clean Google-style road treatment - white road fills on light-gray land with the casings faded out (minor-road casing == the land, so streets are crisp white lines with no outline), soft-yellow motorways, neutralized landuse (no tan residential/commercial blobs), and flattened fill-patterns (Liberty's fern-hatch wetlands + dotted pedestrian plazas โ†’ flat fills, like Google) - plus light/dark recolor, all at runtime (tuned live in a MapLibre GL JS harness against Google, on-device-verified light + dark)
  • ๐ŸŸก MapTiler Streets path stays wired but off (USE_MAPTILER=true to enable, needs the key). Map labels render in Roboto via self-hosted glyphs (Roboto composited over Noto per glyph, ui/map/MapFonts + scripts/build-map-fonts.sh; probe-and-fallback to plain Noto, so the old bundled-style dead end is retired - the blank basemap was its pinned dated tile path, not fromJson)
  • โœ… First-run welcome - a clean branded intro (no tracking / real places & routes / free & open source) with a single "Get started"; shown once
  • โœ… Tasteful donation - a permanent "Support Vela" entry in Settings, plus a one-time prompt that appears only after a week of use ("entirely optional, and this is the only time it'll ask"), trivially dismissed, never blocking. (Onboarding holder; DONATE_URL points at the project's Buy Me a Coffee page, and .github/FUNDING.yml puts the same link behind the repo's Sponsor button; forks point both at their own page). Both the Settings entry and the prompt route through Onboarding.openDonate, which shows a toast if no browser is installed to open the link (a stripped/degoogled ROM often has none) instead of a silent dead tap.
  • โœ… In-app Light / Dark / Follow-system switch (Settings โ†’ Appearance) - sets Vela's theme independently of the phone, so you can run the app dark without flipping the whole OS (the whole app + the map recolor live off one preference, AppTheme/isAppInDarkTheme; persisted). Dark mode recolors every landuse/landcover fill (commercial, school, retail, โ€ฆ), not just a hardcoded few, so no light/cream patches break the night palette (verified on-device)
  • โœ… Smoother place sheet (2026-09-16). Expanding or minimizing a place faded the folding sections through an offscreen buffer, which cost half a frame on a five-year-old phone. The fade is applied per element now, a third off the GPU time per frame.
  • โœ… Shops in a building sit on their own door (2026-09-16). Open data puts every tenant of a building on one point, so a shop in a strip mall landed on the parcel out front. Where a shop's address names a unit, it is now placed on that unit's own address point, and only what is left over is spread apart to stay readable.
  • โœ… A limit on full icons per block (2026-09-16). In a crowded block the best eight places get a full icon at street zoom, sixteen one zoom closer, everything from the zoom after that. The rest stay as small dots rather than disappearing, so a dense downtown stops fighting itself and still shows everything as you zoom in.
  • โœ… Follow stops fighting your zoom (2026-09-16). Opening the app cold flies the map to your location; pinching back out used to fly it straight in again until you panned away. It flies once now.
  • โœ… Where two sources disagree about a shop, the map trusts the better coordinate (2026-09-16). Vela's open data puts every tenant of a building on one parcel point, so a shop in a strip mall can sit tens of meters from its door. In Both mode, when Google's listing for the same place is more than 25 m from the open one, Google's pin is the one drawn and the open twin steps aside, instead of the other way round. The open copy only steps aside when Google's pin actually draws, so a place never disappears from both (2026-09-17). If Google's nearby answer lists a place as permanently closed, the matching open pin is hidden for good.
  • โœ… The turn list opens on the turn you are driving to (2026-09-16). Swiping up the nav bar (or tapping the list button) lands on the current step instead of the first one. Steps you have already driven are still there one scroll up, grayed out, along with any stop band before them. The directions preview from the route chooser still starts at the top with nothing grayed, and under a D-pad focus lands on the current step.
  • โœ… Stops stand out in the turn list (2026-09-16, issue #519). On a trip with stops, the step list draws a tinted "Stop: name" band where each leg begins, between the arrival of one leg and the first turn of the next, so a long list reads leg by leg. Names come from the planned stops, or the stops still ahead while driving.
  • โœ… Traffic overlay draws over buildings (2026-09-16, issue #521). At street zoom the gray building footprints and the 3D extrusions painted over the congestion colors; the traffic raster now sits above the buildings and still below every icon and label.
  • โœ… Use Vela without Google, one switch (2026-09-21). Settings > Privacy > "Use Vela without Google" stops every request to a Google host: search goes to OpenStreetMap and your downloaded regions, places come from Vela's data, routes come from the open router, and Street View, the traffic overlay, satellite close-ups, transit directions, reviews and photos are off. Replaces the four-toggle recipe the FAQ used to give, and reaches the surfaces those toggles could not.
  • โœ… Lighter launch, and a Performance page (2026-09-22). Vela uses about half the memory in its first minute: the voice search model loads when you tap into search, the spoken-directions voice when you open directions, and Google's pages when a search lands. Settings > Performance can load voice search at startup again (on by default on phones with about 8 GB of memory or more) and holds Compatibility rendering.
  • โœ… English names on the map in Japan and other non-Latin countries (2026-09-22). With the app in a Latin-script language, Vela's places show their English or romanized name where OpenStreetMap has one, including every branch of a chain it names once, and the map's own shop labels show one readable line instead of two. The places bake also stopped adding OSM duplicates of shops it already had in those countries, and uses OSM's pin for them.
  • โœ… A name typed while looking far away finds the place near you (2026-09-22). When the map is over another city and nothing there carries the name you typed, Vela also looks around you and shows the match.
  • โœ… Shared Google Maps links open (2026-09-22). A short maps.app.goo.gl link someone sends now opens the place, with Google on or off: Vela asks Google's link shortener once, with no cookies, reads where the link points, and searches the place from its name and pin. With "Use Vela without Google" on, "Open shared Google Maps links" (on by default) can turn that one request off, and a short link then shows a toast. Shared lists still need Google.
  • โœ… Delete all offline data, and deleting actually frees space (2026-09-21, issue #601). Offline maps > Storage has a "Delete all offline data" button: every saved area, every downloaded region (routing, places, map), the building and address overlays, the road features and the browsing cache go, and the map database is packed so the file shrinks. Deleting a single saved area and clearing the map cache pack it too; before this, MapLibre kept the file at its largest size after a delete, so a phone could report gigabytes of map data with every list empty. Voices and speech models are kept.
  • โœ… Offline maps: storage first (2026-09-16, issue #518). The storage breakdown and Clear map cache moved above the state and country catalog, so what the phone already holds is visible before the list of downloads.
  • โœ… Place titles stay in your language's script (2026-09-15). Tapping a place abroad used to retitle the sheet with Google's local-script name (a Hebrew title over a pin the map labeled in English). The sheet now keeps the map's own label when Google's name is in another script than the app language, and takes Google's name when it is in yours.
  • โœ… Open in another map app (2026-09-15). The place sheet's share menu hands the place to any installed map app that handles a geo: link (OsmAnd, Organic Maps, ...) through the system chooser, Vela itself left out of the list. For the people who want Vela for looking things up and a different app for the drive, so the route never leaves the app they trust.
  • โœ… Google-style POI markers - category-colored circles with white Material Icons glyphs (food=orange, shop=blue, park=green, health=red, transit=blue, โ€ฆ), generated at runtime over a bundled Material Icons font, sized to read like Google's (iconSize 0.8); in light mode the POI label text is colored by category too (like Google). Density tuned for parity: Liberty's always-on poi_transit layer (bus stops at every zoom = clutter) is pushed to z16+ like Google, while the next business tier (poi_r7) is pulled down to z15 so more shops/restaurants show; MapLibre's label collision keeps it tidy. Nameless POIs are filtered out (has("name") AND-ed onto each poi layer's rank filter) - the unnamed icons couldn't be opened anyway (they'd just drop a near-by address pin) and read as duplicate junk, so only labeled, tappable POIs render now. Ranked by prominence, not tile order (2026-07-02): when two OSM POIs collide, each poi layer sets symbol-sort-key = rank (the tile's prominence field, lower wins the slot). (But the map's business POIs are Google's, not OSM's - see below; this only affects the OSM fallback when Google POIs aren't showing.) Marker restyle - gray teardrop pin + left-aligned labels, no white ring (2026-07-04). The markers dropped the flat white outline ring and now sit in front of a muted-gray TEARDROP/pin backing (Google gray #9AA0A6) - a rounded body whose point extends below the colored dot, with a soft drop shadow - so each reads like a Google pin (PoiIcons.marker redraw: a Path teardrop = body circle unioned with a tangent-sided triangle to the tip + BlurMaskFilter shadow; the colored dot is the bitmap center so the layer's default center-anchor keeps the dot on the place, no placement shift). And the label now sits to the LEFT of the icon (Google-style) instead of under it - textAnchor RIGHT + a negative em textOffset (scales with the prominence-driven text size so it clears the dot across sizes), applied to both the ambient Google POI layer (VelaMapView) and the OSM poi_r*/poi_transit fallback layers (PoiIcons.applyToLiberty). Device-verified on a Pixel 5a in both themes: white ring gone, blue/orange/gray dots pop on the gray teardrop, labels clear to the left. Labels are UPRIGHT everywhere (2026-07-06). Liberty's default POI text face is Noto Sans Italic, so while ambient Google POIs (which pin Noto Sans Regular) show upright, the moment ambient cleared - a search, a selected place, nav - the OSM poi_r*/poi_transit labels slanted, an "everything goes italic mid-search" flip. applyToLiberty now pins Noto Sans Regular on those layers too, matching the ambient layer and Google (whose labels are upright in every state).
  • โœ… Building footprints + house numbers, Google-style (2026-07-04). Residential/building outlines read sparse because of a real bug: the bundled Liberty building FILL layer is minzoom 13 / maxzoom 14, and MapLibre maxzoom is exclusive, so the code's setMinZoom(14f) collapsed its range to empty (14 โ‰ค z < 14) - the crisp flat footprints never painted, leaving only the faint building-3d extrusion (the "sparse vs Google" look). Fixed by re-opening the top with setMaxZoom(24f) on the building layer in both applyLight/applyDark, so flat gray footprints with a crisp darker outline draw from ~z14 up (overzoomed z14 tiles fill z15+); building-3d extrusions are now gated to z16+ (flat fill carries the browse-zoom look; extrusion is the per-pixel-expensive part on a Pixel 5a). House numbers: the vela-housenumber layer (OMT housenumber source-layer) is confirmed functional - OpenFreeMap really does serve that layer (verified against the live TileJSON + z14 tiles), so it renders where OSM has addr:housenumber; minZoom lowered 17โ†’16 to surface them a touch earlier. Device-verified on a Pixel 5a: a dense downtown renders full building footprints + house numbers at z16+. Coverage caveat (OSM, not the renderer): footprints are near-complete in US/EU metros but sparse in some newer suburbs, and addr:housenumber is patchy - Vela renders whatever OSM has, so a thinly-mapped neighborhood still looks bare (that's an OSM data gap, not a Vela bug).
  • โœ… Satellite and other constrained networks (issue #235). Vela declares itself optimized for constrained satellite data, which is what lets carriers who gate satellite service pass its traffic, and it now behaves accordingly: on a satellite or bandwidth-limited link it stops fetching place photos and switches to a leaner nearby-places query, so search and routing keep working where that link is the only signal you have.
  • โœ… Railway crossings + speed humps on the map (2026-08-08). Two more road aids from OSM, drawn exactly like the lights and stop signs (same close-zoom gate on the browse map, same along-your-route loading during navigation): a crossbuck badge where a road crosses train tracks, and an amber bump badge for speed humps, tables and cushions - the calming shapes you actually feel. Level crossings and humps are static infrastructure, so this works offline-cached and keyless like the rest of the layer.
  • โœ… Traffic lights + stop signs on the map (2026-07-05). OSM highway=traffic_signals draw as a small stoplight icon and highway=stop as a red STOP octagon, beneath the POI dots + pins, at close zoom (z โ‰ฅ 16). Zoom-scaled icon size + draw-all (allowOverlap, 2026-07-06) so theyโ€™re actually visible on the browse map, not culled by the denser POI layer. Keyless (Overpass), fetched per-viewport but area-cached - the fetch box is padded 50% beyond the view and reused while you stay inside it, so panning/driving through a neighborhood doesn't re-hit the server. Zoom-gated (no setting); shows in both browse and nav. During navigation they load once for the whole route (2026-08-08, issue #248): a single corridor fetch along the planned route replaces the viewport churn that made the icons appear inconsistently and vanish mid-drive, and the nav render floor moved just under the auto-zoom's highway-speed level so they can't blink out when the camera pulls back.
  • โœ… Camera facing cones (2026-07-21, user request). Flock units are single-direction, so every direction-tagged DeFlock node (93% of the dataset) now fans a translucent purple cone showing which way the camera FACES, DeFlock's own treatment - the OSM direction / camera:direction tag rides the dataset as a 4th TSV column (cardinals normalized to degrees at bake; 3-column files still decode, so old bundled/hosted data keeps working). The avoid-cameras route count deliberately stays proximity-based: ALPRs commonly capture front AND rear plates, so a camera near the route can read you whichever way it points - the cone is information for your eyes, not a reason to relax the count.
  • โœ… Surveillance-camera (Flock / ALPR) layer (2026-07-12, user request). Settings > Navigation > Cameras > Surveillance cameras draws mapped automated license-plate readers on the map - the community DeFlock project's surveillance:type=ALPR nodes in OpenStreetMap (Flock Safety, Motorola, police plate readers) - as a distinct purple camera badge, visible from route-overview zoom (z11+, 2026-07-13; both the fetch gate and the render layer - an earlier pass lowered only the fetch, so cameras arrived but never drew below z13.5, a gap the vela-dpad fork caught in issue #131). The dataset is bundled on-device (the whole global DeFlock set is ~1.3 MB) and refreshes itself weekly from a hosted copy, so the layer draws instantly with no network and stays current without app updates; live Overpass remains only a brief startup fallback. ON by default since 2026-07-13 (Flock holder) - it costs nothing to draw now and it's a headline feature; turn it off in Settings > Map if you don't want it. Coverage is OSM's/DeFlock's - strong across the US, growing elsewhere. Avoid surveillance cameras (2026-07-12, opt-in): a SEPARATE Settings > Navigation toggle ("Avoid surveillance cameras", OFF by default, FlockRouteAlert) that both warns and avoids. It badges each route option with how many mapped cameras it passes (amber camera glyph + "N cameras on this route") AND auto-prefers the fewest-camera alternate - but only for a MODEST detour (selectRoute the min-camera route when it beats the fastest on cameras and costs at most 25% / 10 min more; a "Rerouted to avoid cameras" toast explains the switch). That cap is where the line is drawn: a small detour to dodge cameras, never an hour around one on a 15-minute trip - and it only chooses among the alternates Google already returns (no impossible "avoid this whole region" routing). Long-press "route through here" stays as the manual override for a specific detour. Keyless: the bundled set (FlockCameras.along) counts the ALPR nodes within 45 m of each route's polyline (120 m until 2026-09-16, issue #527: a camera on a parallel street a block over counted against a route that never passed it), off the hot path. Device-verified: routing through a camera-dense corridor badged the fastest route "18 cameras" and auto-switched to the +1 min, 0-camera alternate. Kept independent of the map layer on purpose (you might want one without the other). Device-verified 2026-07-12 in Atlanta (Ponce City Market): the purple camera badges draw on the map. Two bugs were caught and fixed in that verification: the Overpass query used out tags, which for a node returns id + tags but no lat/lon, so the parser dropped every camera and the layer was always empty - now out body; and Flock.init was missing from VelaApp, so the toggle reset to off on every app restart (the persisted pref was never loaded) - now initialized alongside the other layer holders. (Tap-for-detail on a camera and an approach chime are possible follow-ups; this ships the map layer.)
  • โœ… Map POIs are Google-ranked by real prominence, not OSM (2026-07-02). The map's business POIs come from ambient Google places (nearbyPlaces - an 8-term category fan-out over the viewport), and OSM's poi_r1/r7/r20 are hidden while those show. The bug: the ambient dots collided at the same building (a Safeway and a tiny sushi counter inside it), and the winner was decided by the fan-out's category-term order ("restaurants" queried before "stores"), so the sushi place outranked the Safeway. Fixed by ranking the deduped Google places by real prominence - ambientProminence = ln(reviewCount+1) ยท (0.6 + rating/10) (review count dominates, rating nudges) - so the anchor store (Safeway, ~1,300 reviews) beats its in-store tenant (dozens), and the map's top POIs are recognizable landmarks (device-measured: the regional mall, the big grocery anchors, Trader Joe's, Walmartโ€ฆ) instead of the 0-review junk the fan-out drags in (a mobile mechanic, a care home, a road intersection). A distance-bucketed variant was tried and reverted - it floated near-center junk above the landmarks (caught by an on-device log). Pure prominence, exact distance only as a tiebreak. Unit-tested (AmbientRankingTest); rankAmbientPlaces/ambientProminence are pure :core functions. Deeper + broader + zoom-scaled (2026-07-02): three coordinated levers make the map fill with businesses like Google's. (1) Broader categories - the fan-out gained grocery store / gas station / gym / bar / pharmacy (13 terms) so the map shows a real MIX, not just food/shops. (2) Deeper pool - !7i40 โ†’ !7i60 per term took the candidate pool from ~150 to ~690 (device-measured) so there's rank to descend into. (3) Hand the map ALL of them, let collision do the zoom-scaling - an early cut (take(80) โ†’ a zoom-scaled take(260)) turned out to be the "seeing fewer results" bug: over the ~3.5 km fetch span the deep pool's far, more-prominent places filled a small cap, so a low-commercial view's own nearby shops were cut before the map could try to draw them (device-caught: a residential viewport handed 260 POIs rendered zero, all in the commercial cores off-screen). Now the map is handed the POIs near the view (a viewRadius ร— 1.25 filter, AMBIENT_ONSCREEN_CAP = 140 by prominence) instead of the whole 3.5 km pool - because off-screen POIs can't render anyway, and MapLibre re-runs symbol collision over every handed feature each drag frame. So nearby businesses (incl. the welcome low-signal ones - care homes, a floral counter, a local LLC) render, more appear as you zoom in (dots spread out, fewer collide), and a low-commercial view keeps all its handful of local shops (under the cap โ†’ nothing cut, the "fewer results" fix stays). Low-signal can't crowd out a business (prominence order places real ones first; low-signal fills leftover collision gaps). Perf: the first cut handed the whole pool (~800) and a Pixel 5a lagged dragging - the collider was chewing 800 symbols/frame; view-filter + cap brought it to 1.38 % janky frames, 90th-percentile 15 ms on the same 5a, with identical on-screen density (device-verified gfxinfo + screenshot). (Keyless ceiling: the viewport query floors at a ~3.5 km span - Google's tbm=map returns FEWER hits for a tighter box - so a hyper-dense downtown at max zoom is still bounded by what one span returns; fine for the common case.) Prominence sizing + smoother panning (2026-07-03): four more Google-like touches. (1) Anchors read bigger + show from farther - each ambient dot now carries its prominence to the layer (MapMarker.prominence), driving a data-driven iconSize (~0.78 for low-signal โ†’ ~1.3 for a Safeway/mall) and textSize (11โ†’14pt) via MapLibre interpolate expressions (zero per-frame CPU), plus a prominence-weighted keep-radius (viewRadius ร— (1.25 โ€ฆ 1.6)) so a high-prominence anchor survives off-center and appears at the edge like Google does at 500 ft. Doubles as "bigger POI icons". (2) Smoother drag on the 5a - applyData was re-setGeoJson-ing (a full symbol re-tessellation) on every recomposition (a nav mySpeed tick, a mute/theme toggle); now it short-circuits when the marker/ambient lists are unchanged (structural equality, reset on style reload). With a lighter halo (1.2โ†’0.9) + icon-padding (3โ†’1.5), drag jank on the Pixel 5a went 1.38 % โ†’ 0.47 % janky frames (90th-pct 15 โ†’ 10 ms).
  • โœ… Nav starts instantly again (2026-07-17). Tapping Start could sit dead for up to ~20 s before "Starting navigation," and double-tapping announced it more than once. Cause: making traffic-light guidance standard put a live Overpass fetch (up to a 25 s server timeout, over the whole route) on the critical path BEFORE the session started - a regression. Nav now starts immediately on the plain route and the "pass the light, then turn" clauses fold in a beat later in the background (same geometry, only turn text changes), or are skipped if the fetch is slow - they're best-effort landmark text, never worth blocking the drive. Start is also guarded against re-entry, so a double-tap can't queue a second start.
  • โœ… Rerouting spins (2026-07-17). The banner's refresh glyph rotates while a reroute fetch is in flight - the state was already loud (chime, buzz, "Rerouting" headline, bottom-bar tag) but nothing MOVED, so a slow fetch could read as frozen. Draw-phase animation (the angle is read in graphicsLayer), so the spinner never recomposes the banner and only exists while rerouting shows.
  • โœ… Route swaps record WHY (2026-07-17). Every route block in a trip now carries its reason - "start", "reroute" (a wrong turn), "faster" (you accepted the faster-route offer), "heal" (the silent abbreviated-steps/traffic self-heal), "stop-added" - stamped at the session's swap sites and appended to the RD line (old files and parsers unaffected). Replays use it: only wrong-turn reroutes chime now; swaps that were quiet live stay quiet in replay. The audit prints each swap with its reason and fix index.
  • โœ… Replays chime at route swaps (2026-07-17). The rerouting earcon now plays at a recorded trip's swap points during replay - replay is the nav test bench, and the chime is the audible marker that the route changed here. (Live reroutes were already chiming; replays suppressed the whole live-reroute path by design, so swaps were silent. Caveat: the trip format doesn't distinguish a wrong-turn reroute from a silent faster-route adoption, so replay chimes for both - a marker, not a re-enactment.)
  • โœ… Car-mode strips neighborhood titles; water names stay (2026-07-17). Hamlet and neighborhood labels joined the drive-nav declutter list (the read-by-nobody tier). WATER names were briefly stripped too and restored the same day: rivers and lakes are real landmarks, the user liked them, Google keeps major water names in nav, and a viewport carries a handful at most - the saving was noise. Town, city and state names stay for orientation. Walking and biking keep everything, as before.
  • โœ… Thick-liquid nav camera (2026-07-17). The follow camera's single fast ease (0.12 s on everything) made every road kink a camera rotation within a third of a second - the "squirrely" feel. The time constants are split now, Google's grammar: position stays fairly tight (0.25 s - the puck draws at its own point, so camera lag reads as glide, not error) while bearing (0.55 s) and zoom (0.5 s) get heavy damping - turns sweep around over about a second, the zoom breathes, and the puck's own bearing filter thickened a step so vertex-level polyline jitter never reaches the glass.
  • โœ… Trips are a flight recorder now (2026-07-17). Beyond the GPS trace, route segments, off-route flag and build stamp, a recorded trip now carries: every line the voice actually spoke with timestamps (voice-vs-card questions answer themselves), per-fix GPS accuracy (the accuracy-scaled corridor's decisions become reconstructable), 30 s frame-pacing samples during nav (UI-thread Choreographer cadence: frames, janky count, worst gap - "it felt laggy around the exit" becomes a number), and battery every ~2 min (drain per drive is measurable). All appended formats: old files and old parsers are unaffected, and the audit harness prints each section. Strictly local and opt-in as always - trips never leave the phone unless shared by hand.
  • โœ… Label pass de-hitched + the off-route flag is recorded (2026-07-17). The cross-street pass was itself the "camera dropping frames" report: it materialized every loaded road-name feature on the main thread every 4 s and re-ran symbol placement whenever its sliding window moved a name in or out. It now snaps to 400 m quanta of progress - the whole pass (query, geometry, filter) runs once per quantum or when the upcoming turn targets change, the query carries a class filter so far fewer features are materialized, and the include set is byte-stable between quanta so placement stops churning. And recorded trips now carry the engine's LIVE off-route flag per fix (appended column, old files unaffected); the audit prints the off-route spans - a wrong-turn report is now diagnosable fix by fix.
  • โœ… "tacake" fixed + trips record their build (2026-07-17). "Take exit 186" speaks as "take the 186 exit" - the espeak G2P mangles the bare take-exit adjacency the same way the old take/tyke bug did, and "take the" is the onset it reliably gets right (display text untouched). Recorded trips now stamp the versionCode that recorded them into the CSV header (appended column, old files and old parsers unaffected) and the audit harness prints it - so a "which build said that" question answers itself. Reroutes were ALREADY captured: every route the drive used (start + each reroute or faster-route swap) is its own segment in the trip file, activated at the fix where it appeared.
  • โœ… "Half a mile", and the turn target keeps its name (2026-07-17). Sub-mile spoken distances phrase as fractions - "In half a mile", "In a quarter mile" - instead of "zero point five miles" (quarter-mile granularity below a mile, exactly Google's grammar; the banner keeps the precise figure, metric users already heard natural "500 meters", and other languages keep their own tables until they adopt the fraction forms). And the cross-street label pass got two correctness fixes: the exclusion list is per-step now (roads already DRIVEN), because excluding the whole route hid exactly the label that matters most - the road you are about to turn onto; the next two turns' target roads are force-included (a turn target meets the route at a shared junction vertex, which a proper-crossing test can miss); and the crossing window tightened to 150 m behind / 2 km ahead so callouts hug the road you are on.
  • โœ… Nav labels: true cross-streets only + a steadier puck (2026-07-17). The label bubbles now show ONLY streets whose geometry actually CROSSES the route ahead: every ~4 s a background pass takes the loaded road-name features and intersects them with the route window (300 m behind to 4 km ahead), and the layers' filter tightens to that include-list. Parallel streets and near-misses stop getting callouts, and the "ghosts" stop too - the include set only changes as the drive progresses, so labels stop churning through the renderer's placement fade (also strictly fewer symbols per frame). Verified on a simulated drive: one bubble on screen, the cross street ahead. And REPLAYS stop feeding the phone's LIVE accelerometer into the puck's speed model - a handset on a desk fed sensor noise into the Kalman while a trace played, which was the "jittering very slightly all over" (real drives keep the fusion; replays dead-reckon on recorded speeds alone).
  • โœ… Street View works by D-pad (2026-07-17, from ars18's vela-dpad fork). The panorama gets the fork's engage model: focused, OK engages look-around - arrows pan the sphere, +/- zooms (the same clamps as pinch), OK again WALKS forward when a neighbor pano sits within the facing arc, BACK disengages then closes. Arrows/controls join a predictable focus ring (pano -> close -> fullscreen -> time travel) since the overlaid controls defeat spatial focus movement, walk chevrons and the time-travel chips wear the dpadHighlight ring. Every key path calls the same code the touch gestures use (docs/dpad.md rule 3). Verified on-device with real key events: engage, pan, walk; touch behavior byte-identical.
  • โœ… Baseline profile - nightlies install pre-warmed (2026-07-17). Sideloaded installs get no Play cloud profiles, so every nightly ran interpreter-cold until overnight background dexopt - the "even Settings scrolls laggy, did you R8?" report (the APK was fine; the phone just hadn't recompiled it yet). Now a committed baseline profile (generated by the :baselineprofile macrobenchmark module on a headless emulator: cold start, map pan/fling, list scroll) is baked into every release build and androidx.profileinstaller AOT-compiles those paths at INSTALL time - a fresh nightly is fast from the first launch. A monthly GitHub workflow regenerates the profile on an emulator and opens a PR when it drifts. Generation must NEVER run against a connected phone: the test harness uninstalls the target app when it finishes.
  • โœ… Drive-feedback batch 4 (2026-07-17). The persistent current-road shield moved to the TOP nav card (right of the distance; skipped when the maneuver's own chips already show that route). Bare state refs chip now: "NV 28" style spaced two-letter refs render as stylized badges (the pattern only knew the hyphen form; kept case-sensitive so "on 5" can't become a shield). A rerouting chime: a soft two-note earcon plays when a wrong turn triggers a reroute, before the spoken word - synthesized in-process on the guidance audio stream, silent when guidance is muted. The road you're driving never labels itself: the bubble layers exclude the route's own road names and refs (the highway used to call itself out repeatedly along a drive); only streets you cross get callouts, which is also fewer symbols per frame. The car-mode declutter is now DRIVE-only: walking and biking keep bike/trail accents, house numbers, hillshade, transit lines and the full POI mix - only the car strips them. Verified on a simulated highway drive: top-card shield, exit + interstate chips, no self-labels.
  • โœ… Drive-feedback batch 3 (2026-07-17). The road you're ON shows as its stylized shield in the bottom nav bar for the whole stretch, not only when a maneuver mentions it ("the actual state route/interstate should always have the stylized version"). Voice repeats are trimmed: a step's second and third prompts drop the sign-destination tail ("Take the ramp on the right", not the whole sign again) and a MERGE announces once on approach - getting on a freeway used to narrate the same road in declining counts three or four times. Nav strips more browse dressing for battery (the phone-barely-charging report): basemap highway badges (the wall of shields, including the other direction's), bike-path and trail accents, transit lines, hillshade, 3D building extrusions and the address-overlay numbers all hide while navigating and return when the drive ends; the address-overlay viewport refresh pauses too. Label bubbles got more contrast in dark mode. Unit-tested (merge single-prompt, repeat-shortening). And the cross-street tier FADES with zoom (2026-07-17) instead of popping: the nav camera's zoom is speed-scaled, so the old hard cut made every minor-street bubble appear or vanish at once as speed crossed the boundary - now they ease in over half a zoom level, and only at genuinely slow speeds (Google shows that tier only when zoomed further than nav feasibly sits). Thinned further same day: tertiary roads ride the slow tier too, and bigger collision padding spaces the survivors out - fewer symbols per frame. And the VOICE speaks only the primary sign destination ("toward I 5 North", not the whole "...: Vancouver British Columbia" sign) - the full sign stayed long enough in the mouth that the next prompt cut it off mid-sentence. The card matches (2026-07-17, user-agreed design): the banner HEADLINE is the same spoken form - card and voice can never disagree - with the sign's secondary cities on a dim one-liner below it for confirming against the physical sign; the compound "then" row uses the short form (its chip already names the route), ending its arbitrary mid-sign ellipsis.
  • โœ… Drive-feedback batch 2 (2026-07-17). During nav the map now shows gas stations only - the one POI class worth a glance mid-drive; everything else hid (the full POI layer over the route read as clutter). Pinch-zooming no longer detaches the free-drive follow: zooming while the camera tracks you keeps tracking, just at your zoom - only a genuine pan looks away (the same pan-vs-pinch split nav already made). Turns announce earlier on the open road (~35 s out, was ~25 s; a real-drive A/B had Google calling turns up to 0.2 mi sooner at highway speed - city and walking prompts unchanged). And traffic-light landmark guidance is standard now ("pass the light, then turn right"): the Advanced toggle is gone - when a turn is ambiguous, the light is the landmark, exactly when Google narrates it. Still conservative (1-2 lights, plain surface-street turns only) and English-only for now.
  • โœ… Drive-feedback batch (2026-07-16). Every drive starts heading-up (the compass toggle is per-drive now - a north-up pick used to stick for the whole app session). Floating road labels during nav: horizontal, viewport-aligned street names hover over the roads you cross - legible even with the camera tilted, like Google's callouts (device-verified on a simulated drive). Reworked 2026-07-17 after the first real drive: each name now sits in a real Google-style BUBBLE (rounded chip with a pointer tail touching its road, theme-matched, drawn via a stretchable style image + icon-text-fit so any name length fits), residential cross streets are back (the first cut dropped the "minor" road class for sparseness, which muted the layer on neighborhood drives - minors now show from z16, i.e. at surface-street speeds, and stay out of the highway view), and the basemap's own line-following road names hide during nav so nothing draws twice (placement work saved every frame). Device-verified on a simulated drive: bubbles with tails on the cross streets, no doubles. Basemap POIs stay visible while navigating (gas stations on the way; the master Show-places switch still wins; narrowed to gas stations ONLY on 2026-07-17 - the rest read as clutter over the route). Returning to the app mid-drive re-centers automatically - a stray pan while backgrounding used to leave the camera detached until a manual tap. And ramp instructions say which side: "Take the ramp on the right toward X" instead of just "Take the ramp" (all 15 languages; the banner arrow already pointed the right way, now the words match). A shared onramp whose sign lists BOTH directions of the target highway ("99 North, 99 South") now announces only the branch you take - the folded fork disambiguates the destination, and its lane diagram survives the fold. Highway shields got roomier too (bigger badge, larger number). Google-style shields (2026-09-15): the map's route badges are now drawn by Vela over the sprite's outline-only ones: interstates are a blue shield with the red band and a white number, US routes a white shield, state routes and international refs a white rounded badge (state routes had no badge at all before), and every badge is sized for its number instead of the number squeezing into a fixed badge. Outside the US the badge follows the sign color where the tile knows the network: UK and Irish motorways blue, UK trunk and primary routes, Irish national routes, E-roads and the Trans-Canada green, any other motorway blue, everything else white. Exit numbers (same day): motorway junctions with a number draw a small green exit badge from z12.5, in browse and in nav, so a freeway reads as its list of exits the way Google draws it.
  • โœ… Wrong-way detection + a real "Rerouting" banner (2026-07-16). Off-route used to be judged by distance from the route line alone, so a wrong turn onto a road running close alongside the planned one never registered: no reroute, the old blue line stayed, and guidance kept speaking the old route's turns. The engine now also watches your COURSE - driving 60 deg or more against the route's direction counts as off-route even inside the distance corridor, with the usual debounce so lane changes and normal turns don't trip it. And while rerouting, the TOP banner says so - big "Rerouting..." with a refresh glyph in place of the stale instruction (it was only a small note on the bottom ETA card). The nav puck also stopped tracing every lane-level micro-kink of the road geometry: its drawn position averages a short window along the route, so it glides instead of wiggling side to side.
  • โœ… Reroute buzz (2026-07-10). Going off-route now vibrates as well as speaking "Rerouting": three quick ticks then a long buzz, a pattern unlike any turn cue, so a cyclist who can't hear the voice (or anyone riding muted) still knows the route changed. Fired beside the spoken announcement and throttled the same way (once per burst, silent retries stay silent); honors the per-mode "Vibrate on turns" setting. Demo drives also pass the real travel mode now, so a simulated bike route buzzes exactly like the real ride would (it used to run haptics as Driving = silent).
  • โœ… Accuracy-scaled off-route sensitivity (2026-07-15). How far you can drift before Vela reroutes now tracks the GPS fix's own reported accuracy, the way OsmAnd and Google behave - Google doesn't use a fixed distance at all, it map-matches. The corridor is base + Kร—accuracy, clamped: TIGHT when the fix is clean (a wrong turn is caught fast - driving drops well below the old flat 40 m when GPS is good), WIDE when it's noisy (urban-canyon multipath can't trigger a false reroute). It's also mode-relative - foot rides tighter than bike rides tighter than drive, because the PATH is narrow - so a pedestrian who takes the wrong footpath is caught quickly without over-firing when GPS degrades. (An earlier fixed 22 m/28 m walk/bike pair was dropped as too aggressive; a scaled corridor is the right tool.)
  • โœ… Per-travel-mode "Vibrate on turns" (2026-07-03). The single haptics toggle became four - Driving / Walking / Cycling / Transit - so you can buzz on turns while cycling/walking but stay silent driving. Haptics.cue(type, approaching, mode) checks the per-mode pref (haptics_<mode>). Default = on for walk/bike/transit, OFF for driving (Haptics.defaultFor) - in a car you've got the screen + spoken directions, so a buzz every turn is noise; an existing legacy haptics_on=false still wins.
  • โœ… Place-sheet header + actions, Google-style (2026-07-03, settled after a couple of iterations). Header: name + Save (โ˜…) ยท Share ยท โ‹ฎ ยท โœ• as compact 40dp icon buttons; the name is titleLarge (22sp) with maxLines=2, so even "Starbucks Coffee Company" fits two clean lines beside the icons without ellipsizing. Action pills: a highlighted Directions + short Call / Website (ActionPill, horizontally-scrollable as a safety) - the fast path. Phone number + website live as their own tappable rows below the address (showing "(425) 332-6175" and the domain), exactly where Google puts the detail. The collapsed Hours row no longer lets a long holiday value ("5 AMโ€“1 AM ยท 4th of July (Observed)") squeeze the label into "Ho/urs" - fixed-width label, summary stripped to just the hours (substringBefore("ยท")) + ellipsized. Review-loading copy is the calm "Gathering reviewsโ€ฆ". (Iteration history: tried Save/Share in the header + a full-width Directions button, then all-in-scrollable-pills with the number in the Call pill - landed here: Save/Share on top like the previous build the user liked, short action pills, contact detail as rows.)
  • โœ… Search targets the panned viewport, not GPS (2026-07-03). Running a search now biases to the map center you're looking at (mapCenter ?: myLocation), Google-style - so panning to another neighborhood/city then searching returns results there, not back at your GPS location. Autocomplete suggestions do the same. (The "Search this area" button after a search is unchanged.)
  • โœ… A search that finds nothing says so (2026-07-12). A successful-but-empty online search used to set the result list to empty AND clear the status, so the screen just went blank with no feedback (the "POI list isn't showing up" report). Now an empty online result first falls back to the on-device OSM index (a small local place Google may miss can still surface), and only if that's empty too shows a plain "No results for X" message instead of a blank screen. The offline fallback is now one shared path (offlineSearch) used by both the empty-result and the network-failure branches. Localized in all supported languages.
  • โœ… Faster POI loading via HTTP concurrency (2026-07-03). The ambient-POI load fires ~13 parallel google.com category queries, but OkHttp's default 5-requests-per-host serialized them into ~3 rounds - on a slow connection that's the "POIs take ~10 s to load" report (3 rounds ร— a slow round). Raised Dispatcher.maxRequestsPerHost to 24 so they all fire in one round, and added a 12 s callTimeout so one hung scrape can't stall the whole fan-out. (Device-measured ~2 s per fetch on wifi - that ~2 s is Google's own keyless response latency, the floor; the fix removes the ร—3 serialization penalty that dominates on slower links. Revisits are already instant via the ambient cache.) Multi-area LRU cache (2026-07-03): the single-slot ambient cache became a 16-entry LRU with a 30-min TTL - revisiting any recently-viewed area (not just the last one) repaints its POIs instantly instead of re-waiting the ~2 s, and the TTL stops a since-closed shop lingering all session. (A multi-agent exploration confirmed the honest ceiling: ~2 s per BRAND-NEW area is Google's own keyless response latency and can't be cut without a different data pipeline; the real wins are HTTP concurrency + this cache + not blanking the map. Progressive streaming / single-broad-query / vector-tile POI decoding were explored and rejected as dead ends.)
  • โœ… POI dots reappear instantly on zoom-back (2026-07-03). Zooming out cleared the ambient dots (they're off below z14) and zooming back in showed a beat of small OSM icons before the Google POIs popped in bigger. Now the last fetch is cached (ambientCache) and repainted immediately (re-centered + view-filtered) when you return to a nearby area, so there's no emptyโ†’OSM-flashโ†’ambient "small then pop" - the network fetch then refines it. The settle debounce also dropped 500 โ†’ 300 ms for snappier first paint.
  • โœ… Voice settings decluttered + default speed 0.8ร— (2026-07-03). Settings โ†’ Voice now shows just the essentials (pick voice ยท Test voice ยท System voice settings) with the rest - the playground, the speed stepper, the 900-voice variant picker - tucked behind a collapsed "Advanced voice options" header (most people never touch them). The default spoken-directions speed is 0.8ร— (compiled + calibration v13; briefly 0.72 on 2026-07-06, reverted 2026-07-07), the userโ€™s preferred cadence. And the review-loading copy that read "โ€ฆthis can take half a minute" is now a calm "Gathering reviewsโ€ฆ" / "Reviews ยท N of ~M".
  • โœ… Replay uses the selected voice, not the system fallback (2026-07-03). Replaying a recorded drive spoke through the GrapheneOS system TTS while still applying the voice-speed pref (the "system voice at 0.8ร—" bug) - because replayTrip started nav with a null voice engine (live nav passes the selected one). Now replay wires the neural synth and passes selectedEngine.packageName, so it speaks through the same Vela voice as a live drive. During turn-by-turn ALL POI tiers are hidden (poi_r1/poi_r7/poi_r20/poi_transit โ†’ off while navigating, restored on exit, keyed on the style so a dark/light flip re-applies it): POI labels re-ran MapLibre's symbol collision on every nav camera rotate/zoom and popped in and out at zoom thresholds - Google declutters its map the same way during nav, so the nav view is label-clean now (top-rank poi_r1 was kept at first but still flickered at the threshold, so 2026-06-21 it's hidden too)
  • โœ… Buildings (footprints + 3-D massing) - OSM building data already in the Liberty tiles (building 2-D fill + building-3d extrusion at high zoom), given enough contrast + a subtle outline to read (was colored almost identical to the land), and the 2-D building layer's minzoom lowered to 14 so residential houses surface a zoom-level earlier (~z17 now). Keyless, no API key, no new data/infra. Caveat (two distinct gaps): (1) OpenFreeMap's keyless tiles drop small footprints from low-zoom tiles, so houses only appear zoomed-in (Google shows them at neighborhood zoom because its tiles are richer); (2) more fundamentally, some US residential houses are simply absent from OSM and so render at no zoom - Google has them from satellite/assessor data that was never imported into OSM. Lowering minzoom fixes (1); only (2) richer building data closes the "house is on Google but never on Vela" gap. Options: MapTiler (still OSM-derived buildings โ†’ same data gap, just nicer styling; path wired, off) won't help here; the real fix is our own tiles baked from Microsoft US Building Footprints (~130 M, ODbL) / Overture / Google Open Buildings - free + open, but bulk files โ†’ self-hosting/PMTiles infra (a deliberate decision, not a quick toggle). โœ… UPDATE 2026-07-04 - gap (2) is now CLOSED for downloaded regions: the Microsoft-footprint building overlay (see the Offline section) is baked into PMTiles by CI and rendered beneath OSM, so a suburb OSM never mapped now fills with real house footprints. Device-verified on a Pixel 5a - the same suburban blocks went from bare roads to full footprints after the overlay downloaded.
  • โœ… Terrain relief (hillshade) - Google-style shaded relief from the keyless open terrarium DEM (AWS Open Data, no key, native fetch so no CORS), added at runtime under the road layers (so roads + labels stay crisp) and capped at z16 (terrain context for the overview, gone at street level). Tuned per theme - a soft warm-gray shadow in light, deeper shadows + a cool highlight in dark. Verified in a MapLibre GL JS harness (same render engine as MapLibre Native) against the real DEM tiles before shipping
  • โœ… Two-finger tilt to 3D (2026-06-27) - drag two fingers vertically to pitch the map between flat and a near-horizon 3D view (Google-style). Tilt gestures are now enabled explicitly and the max pitch is lifted to 70ยฐ; browse-camera moves use newLatLngZoom which preserves the pitch you set, so the tilt sticks until you change it. Pinch fix (2026-09-29, #627): a pinch no longer starts a tilt (the tilt used to begin after 8 pixels of drift, now after a deliberate 20 dp drag), and Settings > Map "Tilt with two fingers" turns the gesture off entirely.
  • โœ… Ambient Google POIs on the map (2026-06-27) - the basemap dots/labels are OSM (OpenFreeMap), so Google-only places used to appear only when searched. Now, on a bare, zoomed-in browse map, the visible area's prominent Google places are fetched automatically (one keyless "places" query - ~20 mixed-category hits - capped to 12) and drawn as small Google-style category dots - the same vela-poi-<group> icons as the OSM POIs (forkood, cart=shop, fue etc.), with decluttered labels - so they read as native map POIs, not clashing red pins. Tap to open like any result. Google-fst balance: while ambienGoogle POIs are showing, the OSM business** POI layers (poi_r1/r7/r20)re hidden so the two don't duplicate ("OSM for the map, Google for the businesses"); OSM transit + the whole OSM basemap stay, and the OSM POIs come back when ambient is off (zoomed out / offline / nav / search). Tightly gated** so scrapinstays modest: zoom โ‰ฅ 14, bare map only (no results / open place / nav / replay), debounced 500 ms, skipped for pans under 250 m. On-device-verified (Applebee's, Subway, Safeway Fuel Station as category dots, tap โ†’ place sheet). Coverage + zoom behavior (2026-06-2:** the query viewport tracks the map zoom (nearbyPlaces(center, span) - span โ‰ˆ 9 km at z14 down to 3.5 km zoomed in, with the !1d/!4f/!7i40 pb tightened to match) so zooming into a strip mall fills it with ITS businesses - ~11โ€“25 local hits within 500 m vs 1 with the old wide search (calibrated live). Re-queries on a real pan or zoom change, and the dots clear when you zoom out past z14 (and the OSM POIs come back). Category fan-out (2026-06-27): nearbyPlaces now runs a small parallel fan-out of category queries (places/restaurants/coffee/stores/services/beauty salon) and merges+dedupes them - one "places" query is biased to prominent food/shops and misses whole tiers (the strip mall's plumber, nail salon, IT shop), so the fan-out roughly doubles local coverage (live: 22โ†’52 unique within 600 m); capped at 50. (An on/off toggle is the next option.)
  • ๐ŸŸก Self-hosted PMTiles - the no-key, no-quota Google-look path - remains for later
  • โฌœ Protomaps "Google-Maps-ify" style (road hierarchy โœ…, hillshade โœ…, POI icons โœ… done; this is the bundled-style variant)
  • โœ… Satellite layer - shipped 2026-07-13 via the Layers panel (Esri World Imagery hybrid; see the Layers entry below). Terrain relief โœ… too
  • โœ… Bike routes prefer bike lanes and quiet streets (issue #401, 2026-09-14). Cycling routes now favour signed cycle routes, bike lanes and residential streets over the fastest road, the way Google routes bikes. With a region downloaded the on-device bicycle profile does it with no network; otherwise an open router online is told to stay off busy roads. On by default; Settings, Navigation, "Bike routes prefer bike lanes and quiet streets" turns it off for the plain fastest bike route.
  • โœ… Offline bike routing prefers signed cycle routes again (2026-09-12). The slimmed-down region bake had dropped route relations; they are back, so the on-device bicycle profile favours signed cycle routes the way OsmAnd's does, for 13% more bake time and a few hundred kB per state.
  • โœ… Offline routing regions bake in minutes, not hours (2026-09-11). The region bake keeps only the road network before indexing (highways, ferries, turn restrictions), so a US state that took 48 minutes and did not fit for a whole country now takes about five, and the files are a quarter smaller. Verified offline on a phone: a 40 km drive routed with no signal from a freshly baked state. Long intercity routes are still slow on the on-device engine.
  • โœ… Issue sweep 2026-09-10. Copy link shares the place, not a pin (issue #359): the place sheet's Copy link now copies the same Google Maps place link that Open on web uses, so the recipient lands on the store. Your lists opens from the search page (issue #343): the bookmark button in the search bar did nothing while the search overlay was up. Light-mode search bar has contrast (issue #351): white, with a shadow and a hairline border, like Google's; dark mode unchanged. 24-hour clock (issue #357): the arrival time, transit boards and saved-trip timestamps follow the phone's 12/24-hour setting, not just the locale. Live overview (issue #352): the in-nav Overview keeps refitting to the road still ahead as you drive, tightening towards the destination, until you pan, pinch or Re-center. What's new before you update (issue #330): the update card folds out the release notes. Plus the Reviews/About tab labels and Copy name were untranslated in every language.
  • โœ… Country and state/province borders (2026-09-09, discussion #353) - drawn the way Google draws them: countries as a thin solid gray line, states and provinces lighter and dashed from about zoom 4, both themed for light and dark. County and city limits stay off, as on Google; they were the stray-dash clutter that got every border hidden back in July.
  • โœ… Map rotation/tilt + heading-up mode during nav (tilted follow-camera, speed-adaptive zoom)

Search & POIs (live Google data)

  • โœ… Tapping a place opens that place (2026-09-18). In a plaza, tapping a supermarket could open a coin machine, a coffee counter inside it, or the brand's fuel station. Two reasons, both fixed. The map ranked everything in the touch area by distance to each feature's own point, but a place icon hangs above its point like a pin while a small dot sits on its point, so a dot a few meters away measured closer to the finger than the icon being aimed at; now the map asks what is actually drawn at the pixel under the finger first, and only falls back to the wider area when nothing is there, so small dots stay tappable. And when Vela looks the tapped place up, a listing whose name IS the tapped name now beats a nearer one with extra words, so the store wins over the store's fuel station.
  • โœ… Tapped house numbers keep their own street (issue #231, 2026-08-03). The address built for a tapped house-number label used the number from the label but the street from Google's reverse geocode, which snaps to the nearest addressable point and around corners routinely answers with the neighboring road, producing right-number-wrong-street pins that then routed wrong. The map's own road under the label now vetoes a mismatched geocode street (normalized so Ave and Avenue compare equal); the geocode still supplies the city and zip. Round two (2026-09-14): that veto only applies when the geocode snapped to a DIFFERENT number. When Nominatim answers with the tapped number itself, its street is the address node's own and wins; the veto had been putting a side-street house on the bigger road drawn next to it.
  • โœ… In-app Street View (2026-07-15, keyless, device-verified). The Street View pill on a place opens a real panorama INSIDE Vela - drag to look, pinch to zoom, walk between panos with the on-screen arrows, and go BACK IN TIME through the older captures. It works keyless the way open Street View viewers do: resolve the pano metadata (the JS Maps API's GeoPhotoService.SingleImageSearch by lat/lng, photometa/v1 by pano id for walking), fetch the equirectangular tiles (streetviewpixels-pa.googleapis.com), and texture them onto a GL sphere we render ourselves. No API key, no login; the pano pbs ride the signed calibration channel so a drift is a config fix, not an app release. The capture date shows in the attribution ("Davis, California ยท May 2024 ยท ยฉ 2024 Google") - the copyright year tracks the shown capture, not the current year, and updates when you go back in time; when a spot has more than one capture, a clock chip (raised clear of the attribution so they don't overlap) lists them and switches the imagery. Walking or jumping to another date morphs - the outgoing pano is held and the new one crossfades in over ~300 ms with a subtle forward dolly, so it reads as stepping into the next spot rather than a hard snap. It opens looking across the street at the building, like Google: rather than a raw bearing to the place coordinate (which points down the road whenever Google's geocode sits on the road centerline), the camera snaps to the road-perpendicular on the building's side - the metadata heading gives the street's direction - and the real bearing nudges it up to 60 deg toward the facade, clamped so it can never swing down the road. Walking aims along the direction you moved. It's half-screen over the live map like Google: the pano takes the top of the screen and the map below shows the nav puck with a view cone at the pano's position, centered in the visible strip (the camera pads for the pane) - turn in the pano and the puck+cone turn with you, walk and it hops (the map eases along), and tapping the mini-map jumps the viewer there (pegman-drop: nearest pano at the tap, looking toward what you tapped, or down the street when you tapped the street itself) - with a corner button to go truly full screen and back (the mini-map hides entirely; Back backs out of full screen first; full screen keeps the same horizontal framing and reveals more sky/ground rather than zooming). All of this device-verified end-to-end. Best of all, it usually opens on Google's own pick: the search response's Street View thumbnail URL carries the exact pano id and camera yaw the Google app itself opens, so Vela uses them verbatim - same imagery, facing the same way, zero extra requests. Only when a response ships no thumbnail does it fall back to heuristics: nearest pano, then (if that pano isn't on the address's own street - a set-back geocode snaps to the alley cluster behind the building, which isn't even graph-connected to the frontage) perpendicular probes that adopt the closest pano that IS on the address's street, facing across the road at the building. Walk arrows show in historical views too (the neighbor graph is the base pano's), and they fetch the neighbor BY ID so the year and the picture always match. A place with no imagery gives a brief "no Street View here" toast. (The old approach - embedding Google's own WebGL page in a WebView - rendered black on many devices and was reverted; rendering the tiles ourselves is what makes it work.)
  • โœ… Toggles to hide reviews and skip photo loading (2026-07-08, user request). Settings โ†’ Map gains two switches, both on by default. Show reviews off = the place sheet renders no review section at all (no tab, no featured quote, no "Read all reviews") and Vela never runs the review scrape for a selected place. Load photos off = no hero strip or gallery, and the WebView photo fetch (the heaviest per-place request) never starts; the search response's preview photo is hidden too, so this doubles as a data saver. Both persist (ShowReviews / LoadPhotos holders, same shape as the other reactive settings) and gate BOTH the fetch in the ViewModel and the render in the place sheet, so off means no traffic, not just hidden UI. Device-verified: with both off, an Applebee's sheet opens with rating, hours, phone and attributes but no photos and no review section. Localized in all supported languages.
  • โœ… "Hide adult categories" toggle (2026-07-08). Settings โ†’ Map, off by default. On = drops places whose Google CATEGORY is adult / nightlife / alcohol / gambling / smoking (bars, clubs, casinos, liquor stores, hookah, cannabis, adult, โ€ฆ) from both search results and the ambient map. Matching is on the free-text category only, never the name (a place categorized "Restaurant" is always kept), and it's PRECISE - food "โ€ฆbar" categories (sushi/juice/coffee/salad bar) stay. Because Google returns the category localized (hl=<lang>), the keyword list carries the equivalent terms for all supported UI languages, so the filter works in every locale, not just English. Pure :core CategoryFilter (unit-tested) applied at the data-source seam (GoogleMapsDataSource.search + nearbyPlaces); gated by a :core-visible enabled flag the HideAdult holder flips, so :core needn't depend on the app's reactive state. Localized in all supported languages.
  • โœ… "Hide website & external links" toggle (2026-07-08). Settings โ†’ Map, off by default. On = place pages don't show the Website pill/row, the Street View pano, or the Book / Reserve / Order action, so no place-detail control launches an arbitrary external site. Internal actions (dial, directions, share a geo: pin) are unaffected. Plain HideExternalLinks holder (ui/PlaceContent.kt, same shape as ShowReviews/LoadPhotos). Localized in all supported languages. (Adapted from a community PR; the PR's separate "restricted" build flavor was not taken.)
  • โœ… Place search - name, category, full address (street, city, state, ZIP), rating, review count, coordinates
  • โœ… Searching a specific/far address resolves to that single geocoded location (handles the response's single-result shape, not just the POI list - fixes the old "calibration error" on far addresses); genuinely-empty searches now show "no results" instead of an error
  • โœ… Address โ†’ business snap - searching a raw address that is a business (e.g. "1020 Olive Dr, Davis") now lands on the business (In-N-Out Burger, rating/hours/category and all), not the bare address - Google lists the "at this place" business under the geocoded node ([0][1][0][14][68]) and Vela now reads it. (verified on-device; unit-tested; the path is in calibration so it's remotely fixable)
  • โœ… Business name stripped from the address line (2026-07-01) - some places' formatted address comes back name-prefixed ("Safeway, 1451 W Covell Blvd"); since the sheet already shows the name on its own line, that read the name twice. SearchParser.stripNamePrefix drops a clean name prefix (only at a real word boundary, and never if it'd empty the line) and the component-array fallback filters out any component equal to the name. (unit-tested)
  • โœ… Search-result rows show 5-star rating, color-coded open/closed status, and the full address (city/state/ZIP) to disambiguate similar names / lookalike residential addresses - sized for legibility (name at titleMedium, the rating/category/address lines bumped up from the cramped small text)
  • โœ… Place sheet (Google-styled): high-contrast white-on-dark / black-on-white name + status time (fixed palette, not washed-out by Material You), 5-star rating visual, three-detent swipe (2026-07-07): expanded, peek, and a small minimized card - a gentle swipe down steps one detent (expanded to peek to minimized), and the hardest flick lands straight on minimized from anywhere. Since 2026-07-08 a swipe NEVER closes the sheet - minimized is the floor, matching Google (the strongest inertia minimizes; only the X or back dismiss), so a flick can't accidentally destroy the place you were reading. A single tap anywhere on the minimized card pops it back to peek (2026-07-08). The minimized detent is a compact card - name, rating, and a Directions button, not the photo hero (device-tested: at that small height leading with the photo strip showed only photos and let the horizontal gallery swallow dismiss drags, so the minimized state short-circuits to a clean header). Works from the handle or anywhere on the body (a nested-scroll handler; mid-list it just scrolls), status with the word color-coded (Open green / Closed red) and the time in plain ink, distance from your location (when opened from a located search) + price + category, full address with a copy button, collapsible weekly hours (today first, expand for the week), and a holiday-hours callout - when a day this week carries Google's special-hours label (parsed from day[6][1], e.g. "4th of July (Observed)"), the place card flags the SOONEST one Google-style in amber right under the status: "4th of July (Observed) ยท tomorrow ยท 9 AMโ€“5 PM" (device-verified on Chase Bank, 2026-07-02), instead of leaving it buried on one day's row
  • โœ… Full-screen photo viewer: pinch-to-zoom (+ pan when zoomed) and swipe-down-to-dismiss, swipe sideways between photos
  • โœ… System maps handler - Vela registers for geo: URIs and Google-Maps web links (/maps/place, /search, ?q=, @lat,lng, maps.app.goo.gl), so tapping an address or "open in maps" in any other app offers Vela. A query runs a search (biased to any coords in the link), a bare point drops a reverse-geocoded pin - the degoogled-replacement piece. (MapLinkParser in :core, unit-tested; parsed in MainActivity.) Two intent filters since 2026-09-28 (issue #614): Android matches each host in a filter against each path in it, so the one shared filter held maps.app.goo.gl and maps.google.com to /maps and those links never offered Vela. Android 12+ opens unverifiable domains in the browser until they are added under the app's Open by default page; Settings > Privacy > Google Maps links shows the state and opens that page.
  • โœ… Directions links open the route chooser (issue #632). Another app's "directions" or "navigate" link (Telegram's Directions button, Google's maps/dir and ?daddr= web links, google.navigation:) opens Vela's route chooser on the destination, with the travel mode and start the link names; before, Vela opened on a bare map.
  • โœ… Viewport-biased "near me" search
  • โœ… Recent searches (persisted) + recently-viewed places - opening a place records it; the search page shows a Recent section (pin icon, one tap to reopen - enriched via search) above Recent searches (clock icon). Capped at 8, deduped, cleared together (verified on-device)
  • โœ… Full-screen search page (Google-style) - focusing the search box opens an opaque page with Home/Work shortcuts, saved + recent searches over the map (back arrow / back gesture closes it); running a search drops back to the map with the results list + red pins
  • โœ… Home / Work shortcuts (Google's signature) - two pinned rows at the top of the search page. Unset shows "Set home/work address"; tapping arms an assign mode (a "Search for your home address" banner + Cancel) and the next place you pick - a suggestion, a saved place, or a tapped POI - gets pinned. A set shortcut shows the place name and a โ‹ฎ menu (Change / Remove); tapping the row opens the place. You can also set the currently-open place as Home/Work straight from its place-sheet โ‹ฎ overflow, or promote a Saved place via its โ‹ฎ. Persisted in PlaceShortcutStore (vela_shortcuts prefs), so they survive restarts (verified on-device: set Home โ†’ "Sacramento Valley Station", โ‹ฎ โ†’ Change/Remove)
  • โœ… Autocomplete / suggestions as you type - after a short debounce, the search page shows live place matches (name + address) to tap, like Google; tapping one opens its sheet directly. Reuses the calibrated search endpoint (no separate suggest RPC); a stale response is dropped if the query moved on. (verified on-device: "starb" โ†’ Starbucks locations)
  • โœ… Clear-search (X) button; the results list is a bottom sheet with the place sheet's detent grammar (2026-07-08): MINIMIZED ("N results" bar) โ†” PEEK (~42%) โ†” EXPANDED (~82%) - tap the handle to step up a detent, drag up/down to grow/shrink one, and a nested-scroll handler shares the gesture with the list. The header has the detent chevron and, to its right, an X that exits the search entirely (results, pins and query - same as backing all the way out); the back gesture still peels detents first. (X added 2026-07-08)
  • โœ… Result filters - chips in the results header: "Open now" (places open right now) and "4.0โ˜…" (rating โ‰ฅ 4.0); they stack and the count updates live
  • โœ… Back gesture peels one layer at a time (steps โ†’ navigation โ†’ route preview โ†’ place sheet โ†’ results list) instead of closing the app - only the bare map exits
  • โœ… Full reviews - now via a hidden WebView, WITH per-review photos (2026-06-27). Google deleted the keyless listentitiesreviews endpoint (it now 404s) and moved reviews behind a batchexecute RPC (rpcids=T4jwAf) whose request proto resisted capture. So reviews are now read the same way as photos/transit: WebReviewsFetcher loads the place's canonical ?cid= page in a hidden, anonymous WebView (desktop UA), lets Google's own JS render the reviews, and scrapes each review card from the DOM - star rating, author, relative date, text, and the reviewer's uploaded photos - into JSON that ReviewsWebParser (:core) turns into Reviews. This finally delivers per-review photos (the old endpoint only ever served avatars - see the strikethrough below). On-device-verified on a neighborhood park (real review text + photo rows). (Wired into MapViewModel.fetchReviews; the cid = the low half of the 0x..:0x.. feature id as decimal. The old listentitiesreviews path/ReviewsParser/calibration reviewsPb are now dead but left in place.) pulled from Google's keyless listentitiesreviews endpoint by feature id. Review photos are collected by URL shape, not a fixed index (recalibrated 2026-06-20): ReviewsParser takes only a reviewer's uploaded FIFE photos (/gps-cs, /geougc, /p/AF1Qip) and never their avatar (/a/, /a-/, ACg8oc, ALV-). This fixes a bug where the old "photos hang under review[12]" calibration was sweeping the reviewer's profile picture into the thumbnail strip - every review showed the author's face as if it were an uploaded photo. โš ๏ธ This RPC turns out to return only avatars, so the strip currently shows nothing; real per-review uploaded photos need a separate source (the photo-inclusion flag resisted discovery - see ROADMAP). Unit-tested (avatars excluded at [0][2]/[12]/[60], genuine UGC collected). Intermittent "no reviews" fixed (2026-06-21): the RPC sometimes returns an empty page (a bot-degraded reply / rate blip), which used to stick as "no reviews" until you reopened the place (seen on a park and a cafe listing). fetchReviews now retries with backoff (up to 4 tries across ~3 s - widened after a tap-to-retry confirmed a fresh call clears the flake within seconds) when the place's own review count is >0 but the fetch came back empty - that count-vs-zero mismatch is the transient-miss tell - while a genuinely review-less place (count 0/unknown) still stops after one try, so review-less places are never hammered. Plus an honest UI state: when the count says there ARE reviews but the list is still empty, the tab shows "Couldn't load reviews. Tap to retry." (a load failure, distinguished from a real zero by the count-vs-empty mismatch) instead of the misleading "No reviews available", so a sticky outage that outlasts the auto-retry is one tap from recovering - MapViewModel.retryReviews(). (Seen showing rating + count but no list on the same park; the count proves the featureId loaded, so it's purely the RPC flaking.)
  • โœ… Save my parking spot (2026-07-08, v2 same day, device-verified end to end). A dedicated P button above the locate FAB: tap saves the spot where you stand (toast; prefs, survives restarts) and the button turns teal while one is set. The map draws a teal "P" pin at the spot; tapping the pin (or the teal button) opens a "Parked car" place sheet with a car glyph beside the name, the normal Directions action (defaults to WALK for a parking spot) and a Clear parking pill. The camera frames the spot when opened from afar. All strings in all locales. (v1 was a long-press on the locate FAB + a chip - reverted: undiscoverable, and Compose's FloatingActionButton consumes the down so outer long-press detectors never fire.) Re-parking hub (2026-07-11, user - "setting parking again when you already have a spot is a little clunky"). With no spot set, a tap still saves where you stand in one go. With a spot already set, a tap now opens a small menu on the P button instead of jumping straight to the parked-car sheet: Find my car, Move parking here (overwrites the spot with your current fix; the old one is not lost - it drops into history), Earlier spots (shown only when history holds a prior one), and Clear parking. So re-parking is one obvious choice rather than the old clear-then-tap-again dance. Long-press still jumps straight to the history sheet. New menu strings are English-first (localization backfill tracked with the other i18n leftovers).
  • โœ… Local place lists (issue #1, 2026-07-08, device-verified). Google-Maps-style saved lists: a Your lists section on the search page, each list a colored icon + name + count that opens as results. Create/edit via a dialog with a name, a 10-icon picker and an 8-color picker (the "custom icons" the requester asked for). Any place's โ‹ฎ menu has Save to list (a checkbox chooser, create-new inline, live count) and Edit note once it's in a list; the note shows on the sheet as an italic quote. Persisted in vela_lists (PlaceListStore). A pasted Google Maps share link previews as results with a Save list pill - nothing is added to Your lists until you tap it (opt-in, 2026-07-09), then it's a durable local list. Remaining: reorder, share-out, a dedicated full-screen list view.
  • โœ… Voice search mic (2026-07-10). A mic in the search bar: tap it, a voice-input app you have (FUTO Voice Input, Google's recognizer on GMS phones, โ€ฆ) captures speech and the text runs as a search. Vela records nothing itself, the provider does, so no microphone permission. The mic only appears when it can actually work: a Settings toggle (Search section, on by default) AND an app that handles the recognize-speech intent. With no such app the mic is hidden, never shown dead, and Settings says why. Keyboard-only dictation (Sayboard, FUTO Keyboard) doesn't trigger it since apps can't invoke a keyboard's mic; those users keep using the keyboard mic, and a later build adds an on-device model so voice search works with no third-party app at all.
  • โœ… On-device voice search (2026-07-10). Voice search now works with no other app at all: the mic is always in the search bar, and with nothing installed tapping it offers the one-time 58 MB Vela voice download. Once it's there, tapping the mic records and transcribes entirely on your phone with a bundled Whisper model, then runs the text as a search. Nothing is uploaded, no account, and it covers all of Vela's languages. A listening sheet shows the mic pulsing with your voice; it stops on its own after a beat of silence (or tap Done), and the microphone permission is asked only the first time you tap the mic. When both Vela voice and a voice-input app are present, a Settings picker chooses which to use (Vela voice by default). Device-verified end to end: download, install, the mic appears, the permission prompt, the listening sheet, and the model transcribing back into the search box. Pauses your music while it listens (2026-07-12): starting dictation takes transient audio focus (AUDIOFOCUS_GAIN_TRANSIENT, assistant usage) so whatever is playing pauses like a phone assistant does, and gives it back the moment the utterance ends so playback resumes.
  • โœ… Pick your dictation engine: SenseVoice + Moonshine (2026-07-20). On-device voice search is no longer Whisper-only. Settings โ†’ Search lists three engines you can download and switch between, all running on-device through the same bundled sherpa-onnx runtime: Whisper tiny (the multilingual default, every language Vela supports), SenseVoice (more accurate + faster, for English/Chinese/Japanese/Korean/Cantonese), and Moonshine (lowest latency, English only). Each shows its languages and size; the first one you download becomes active, and a Use/Remove control switches or frees them. Whisper stays the default so no language regresses, and the other two are opt-in. The recognizer rebuilds automatically when you switch engine or change app language.
  • โœ… Transit lines are opt-in now (2026-07-10). The purple rail highlight is off by default; it reads as mystery lines if you don't know what it is. Settings โ†’ Map โ†’ "Highlight transit lines" brings it back, and an earlier choice is kept either way.
  • โœ… Language switching leaves nothing behind (2026-07-10). Switching back to English used to keep dates (parking history months) and freshly fetched place names in the old language until the app was killed; now the switch back is complete immediately. The translations also lost their machine-looking long dashes across all ten languages.
  • โœ… Pick your voice app (2026-07-10). Settings โ†’ Search lists Vela Voice, "Android default" (your system-wide choice, untouched), and every installed voice app by name as a manual override. The mic launches exactly what the picker says.
  • โœ… The place card follows your finger (2026-07-10). Dragging the place sheet moves it with your finger and, on release, it coasts on the fling to whichever size is closest - no more stepping between sizes in fixed hops.
  • โœ… Live gas prices on the map (2026-07-10). Searching for gas stations shows each station's current price in the same bubble marker the restaurant ratings use, and the full price ("$5.34/Regular") gets its own bold pump-glyph line under the address in the result list and renders bold on the place page's price line. The price rides the keyless search response (place node [88][0], a remotely recalibratable path). EV chargers were checked too: the keyless response strips charger details (price, kW, availability) the way it strips popular times, so those stay marker-only for now.
  • โœ… Search results are Google-style red markers with real glyphs (2026-07-11). Searching "restaurants" pins the map with named results instead of anonymous pins: every result keeps its gray teardrop and category glyph with the circle turned red, rated restaurants get a wide speech-bubble marker with the rating beside the circled glyph, and in a dense downtown the lesser results collapse into little red dots that expand back into pins as you zoom, so the view never turns into a pile of overlapping icons. The base map's OSM icons now BLEND with the Google dots instead of hiding wholesale: they yield only while the viewport truly sits inside the area the Google fetch covered, so panning or zooming past it keeps icons everywhere and fresh fetches merge in as they land. Stop signs and traffic lights also hold back on the browse map until true street zoom (they stay at nav zoom during turn-by-turn, where they're an aid rather than clutter). Result labels stay plain ink (only ambient POI labels take the category tint, like Google); every POI label on the map (results, ambient dots, base-map icons) can now sit left, right, below or above its icon, whichever side is clear, instead of vanishing when its usual spot was taken; and while a result set is up the base map's own POI icons, stop signs and traffic lights all step aside so the results are the only things on the map.
  • โœ… Traffic in words, not just color (2026-07-10). Route choices now say "light traffic", "moderate traffic" or "heavy traffic" to match the green/amber/red time, so the conditions read without relying on color. And a freshly downloaded voice no longer speaks a sample on its own - only picking a voice in the library (or the Test button) plays one.
  • โœ… The route chooser swipes out of the way (2026-07-11). Swipe the chooser down, from its body or its handle, to shrink it to a slim bar with a Start button and see the whole route on the map; it follows your finger and rides the throw, like the place card. Swipe or tap to bring it back.
  • โœ… Arrival time front and center (2026-07-10). The directions panel's "Arrive at 5:30 PM" is bigger and bolder, and the redundant "current traffic" note under it is gone (the route ETAs already show traffic). The faster-route offer during navigation joined the tidy notification stack too, so it can't sit under the turn card.
  • โœ… The results list moves like the place card (2026-07-11). Dragging the search results follows your finger and rides the throw's inertia to the nearest size, minimizing included, and the back gesture exits the search in one press instead of stepping through sizes. Grabbing the map GLIDES the list down to its bar so the map is yours to look at, and does the same to an open place card (down to its small state); the bar or a drag brings them back. The minimized bar now says WHAT it's holding: the search text (or list name) leads in full ink with the result count under it, instead of an easy-to-miss dim "20 results".
  • โœ… One tidy notification area (2026-07-10). Heads-up messages, download progress, update offers and notices now stack below the search bar and chips (or just below the turn card during navigation, whatever its height), each dismissed on its own, instead of painting over each other. A missing-voice warning carries a "Get a voice" pill straight into the voice library.
  • โœ… Right fix for a missing voice, per language (2026-07-11). The missing-voice pill now forks by cause. A language Vela can train a Piper voice for (French, Russian, โ€ฆ) still gets a "Get a voice" pill into the library. A language Piper can't do (Japanese: espeak-ng has no Japanese phonemizer, so there is no Vela ja voice) gets a "System voices" pill that opens the phone's own text-to-speech settings instead, because that is where Japanese guidance is spoken from (the system-TTS fallback) and the Vela library would be a dead end. PiperCatalog.hasVoiceFor(lang) drives the fork; the pill deep-links com.android.settings.TTS_SETTINGS (falling back to the main Settings screen on ROMs that lack it). So Japanese turn-by-turn works today through Google's (or any) system Japanese voice, and the app points you to install one if it's missing, rather than nudging you toward a voice that can't exist. A fully-offline bundled Japanese neural voice (Kokoro) stays a possible follow-up in ROADMAP.
  • โœ… Spoken directions toggle (2026-07-10). Settings โ†’ Voice has an on/off switch for spoken navigation; it remembers your choice, and the mute button during navigation is the same switch.
  • โœ… Update button is a filled pill (2026-07-10). The update card's Update action is a filled pill instead of tinted text, so it stands out by shape and fill rather than color alone.
  • โœ… Recents you can prune, with addresses (2026-07-10). Every recent search and recently-opened place has an X on its row to remove just that entry (Clear recents still wipes the lot). A real place shows its street address in smaller text under the name, and the Home/Work rows show the saved address too. The three-dot menus on Home/Work match the row text color now.
  • โœ… Route chooser and search stay coherent (2026-07-11). Opening any new place, from search, a suggestion, a pin, or Home/Work, closes an open route chooser instead of leaving it covering the fresh place with a stale route. The locate and parking buttons also stay hidden under the chooser and step list instead of drawing on top of them.
  • โœ… Warns when you'd arrive near closing time (2026-07-10). Starting navigation to a place that closes within an hour of your arrival (or before you'd get there) shows a heads-up card and speaks it: "closes at 9:00 PM and you arrive around 8:40 PM". Reads the closing time from the place's own status in any app language; says nothing when the place is closed already or has no hours.
  • โœ… Approximate location explains itself, and the blob actually shows (2026-07-10). Granting approximate (at setup or from the locate button) now gets one plain dialog saying what it means - wide circle for a dot, no turn-by-turn - with an Allow-precise shortcut that goes straight into Android's upgrade choice. And the accuracy circle itself is finally visible: the locate zoom-fit had a pixel-density bug that always framed the circle bigger than the screen, so it read as nothing at all.
  • โœ… Notifications are asked during setup (2026-07-10). Turn-by-turn keeps your next move in a notification, so onboarding now asks right after location; declining gets one are-you-sure and never blocks anything. Heads-up cards also drop below the turn card during navigation instead of colliding with it, and a mid-drive voice download stays visible instead of silently hiding.
  • โœ… Navigation asks for precise location instead of failing silently (2026-07-10). Turn-by-turn needs GPS, and with approximate-only permission it used to start and sit at "Searching for GPS" forever. Tapping Start now explains it plainly and offers to allow precise location; Android shows its own upgrade choice and navigation begins as soon as it's granted. And if location is fully denied, the locate button explains itself and links to system settings instead of doing nothing.
  • โœ… Approximate location shows honestly (2026-07-10). If you grant only approximate location (or the fix is just weak), the map now draws a soft translucent circle around the dot at the fix's real uncertainty radius, so "somewhere in this area" looks like what it is instead of a falsely precise point. Normal GPS keeps the plain dot, and the circle never shows during navigation.
  • โœ… Location asked at "Get started", not on the map (2026-07-10). The location permission used to pop the instant the map first loaded, out of nowhere. It now fires as soon as you tap Get started on the welcome screen: the welcome screen already says what Vela is, so a maps app asking for location right there needs no second explanation (an earlier build put a separate rationale screen in between; that was one tap too many). Declining leaves search and browse working, and the locate button re-asks when you tap it; granting only approximate location works too (the map centers on a coarse fix).
  • โœ… Honest voice-setup choice (2026-07-10). The first-run voice offer used to be download-or- dismiss; now it's a real two-way choice with the recommendation shown by emphasis, not by hiding the alternative: a prominent "Download Vela voice" and a quiet, still-functional "Use existing voice" that keeps whatever text-to-speech the phone already has.
  • โœ… Shorter onboarding, no wall of asks (2026-07-10). First run is now just welcome โ†’ location โ†’ voice, then the map. The old offline-maps prompt and the diagnostics/trip-recording prompt were removed from onboarding: a brand-new user has no context for them yet. Both settings still live in Settings (off by default), and the diagnostics ask now appears in context on the crash-report card - if Vela actually crashes and diagnostics is off, that card offers to turn it on so the next one carries detail. Fewer things to read before the app is useful.
  • โœ… Dialog buttons are Google-style pills (2026-07-10). Every confirm button across the app's dialogs (voice, location, diagnostics, trip recording, โ€ฆ) is now a filled pill and the decline stays a plain text button, so the recommended action reads at a glance. Long labels wrap the pill to its own line instead of breaking mid-word, and it all still works by D-pad on a feature phone.
  • โœ… Settings decluttered (2026-07-10). The Settings page was one very long scroll. The rarely- touched toggles now live in two collapsed groups at the bottom: Advanced (3D buildings, traffic- light guidance) and Developer (simulate driving, simulate location, save trips - the demo/testing tools, each labeled "turn off for real use"). The content filters (hide adult categories, hide website & external links) sit with the other place-content toggles under Place pages. Everything else stays one tap away. Offline moved up near the top since you reach it often. Language is now a simple "Follow system language" switch that only shows the full language list when you turn it off. And the voice library is its own screen now (a "Browse voices" button in the Voice section) instead of a giant list unrolling inline. All of it still works by D-pad.
  • โœ… Update from Settings directly (2026-07-09). The Version section's "Check for updates" used to answer "update available, go back to the map to install it" - now the full offer renders inline: version line, Update button, live download progress, Not now. Same state and download call as the map card, so starting it in either place shows progress in both.
  • โœ… One chronological Recent list + saved-row contrast fix (2026-07-09, device-verified). The search page's separate "Recent" (places) and "Recent searches" sections merged into ONE list ordered by when you used them, the way Google mixes them - a pin icon marks a place you opened, a clock marks a query you typed. Both recents stores now stamp entries with a timestamp (new pref keys; the old-format data stays in place so a downgraded build finds it instead of wiping it, and old entries migrate once with synthesized order-preserving stamps). Also fixed: starred places on the search page drew their name in BLACK in dark mode - the page is a plain background Box, not a Surface, so a colorless Text falls back to black; the row now uses the same on-surface color as every neighboring row in both themes.
  • โœ… Material You dynamic color (issue #15, 2026-07-09, device-verified light + dark). A Settings -> Appearance toggle (Android 12+ only) tints every themed surface - buttons, chips, FABs, dialogs, the focus ring, the nav notification's arrow tile and accent row - from the system wallpaper palette. Off by default (Vela teal stays the stock look). Accents stay legible because everything uses the scheme's paired slots (primary/onPrimary etc.), which Android generates at accessible contrast in both themes; the dynamic scheme is also sanity-checked (background luminance must match the requested theme) and falls back to Vela's own colors on ROMs that hand back a broken palette (seen on GrapheneOS once). The tinting is deliberately split: chrome and transient surfaces follow the wallpaper (search bar, menus, dialogs, chips, FABs, Settings, the notification), while the place/results sheets keep their fixed grays and the map keeps its own colors - those are reading surfaces full of meaning-bearing color (open/closed status, stars, the route line) that has to stay legible on every wallpaper. Worth knowing: Google Maps itself never adopted dynamic color at all, so even the chrome tinting goes further than the app Vela replaces.
  • โœ… List notes stick to chain stores + show in the list (2026-07-09, device-verified). Notes on a chain place (a Safeway with several co-located Google listings) silently vanished: list membership was keyed on a volatile internal id that could point at a different listing next visit, so the note wrote to nothing. Membership, notes, add/remove and the "Saved" affordances now match on the STABLE Google feature id first (the volatile id stays as fallback for places without one), and a saved place opened from the map or search carries its list note onto the sheet. An open list also shows each place's note under its row (italic quote) and leads the results sheet with the LIST'S NAME ("Sightseeing ยท 8"), not a bare results count.
  • โœ… Nav notification v2 (2026-07-09, device-verified). The ongoing notification now shows a large maneuver ARROW (white on teal, drawn per turn type incl. U-turn/merge/fork/roundabout/ arrival flag), the turn instruction with distance, and "N min - X mi - Arrive HH:MM". Teal accent, lock-screen public, no stale timestamp. Demo-drive mode now runs the same foreground service so the notification is part of the demo (and screenshot-verifiable without a drive).
  • โœ… Self-hosted F-Droid repository (2026-07-09). CI builds a signed F-Droid repo index and publishes it to GitHub Pages (fdroid-repo.yml); users add https://pimpinpumpkin.github.io/Vela/repo to any F-Droid client (fingerprint in FDROID.md). Serves latest stable + the newest nightly when it's ahead, and suggests the stable: a default install updates weekly on stables, while the nightly rides the same index as an unstable version for users who enable unstable updates for Vela in their client (same stable-first default as Obtainium). Rebuilds automatically after every successful CI run and weekly promote (a plain release trigger never fired for CI-created releases - GitHub's anti-recursion rule). The index key is a dedicated repo keystore (~/.vela-signing/fdroid.p12, secrets FDROID_KEYSTORE_BASE64/_PASS); APKs keep the normal Vela signature so install sources are interchangeable.
  • โœ… Lists map shortcut + export/import + light-mode status bar + nightly updates (2026-07-09). A ribbon (bookmark) button leads the category-chip row and opens Your lists in its own dialog (lists no longer clutter the search page). Lists export/import to a JSON file from Settings (same flow as saved places). The system status-bar icons (clock, wifi, battery) now flip DARK in Vela's light theme - they were white-on-white and unreadable over the light map. And Settings -> "Include nightly builds" points the self-updater at the newest prerelease instead of only stable, for users who want the freshest CI build. (Superseded 2026-08-07: the toggle is now the three-way "Update channel" picker, see the CI entry.)
  • โœ… Parking history (2026-07-08, device-verified). Every "save parking" is kept in a capped, newest-first history so an accidental overwrite is recoverable: long-press the P button for a restore menu, or Settings โ†’ Parking history to restore/delete individual spots. Clearing the current spot never wipes the history.
  • โœ… Import a Google Maps shared list (2026-07-08, first cut, device-verified). Paste a maps.app.goo.gl share link into the search bar and the list's places land as results - the list title fills the bar, the map frames the pins, and each place opens/saves like any search hit. The owner's per-place NOTE ("I don't wanna go here alone!") survives the import and shows on the sheet as an italic quote under the address - the part of a shared list Google's own export throws away. Fully keyless: the share link resolves logged-out and the page embeds a ready-made entitylist/getlist request (no pb construction; see SPEC). Unit-tested against a captured payload. Next: first-class local lists to import INTO (issue #1's real ask).
  • โœ… In-store department hours (2026-07-08). A department store's sheet lists each department under the main hours - Pharmacy, Fuel Station, Liquor, Delivery/Pickup windows - each with its own color-coded status ("Closed ยท Opens 9 AM Thu") and an expandable 7-day table, split shifts included ("9 AMโ€“1:30 PM, 2โ€“9 PM"). The whole block is NESTED inside the main Hours expansion (2026-07-08 follow-up), so the collapsed sheet stays one clean line and the full schedule story lives behind one tap. Parsed from the whole [118] response list, the same block whose FIRST entry used to be misread as the store's status (the "closes soon at 10 PM over a 5 AMโ€“1 AM table" bug - the fix and this feature are the same discovery); store-name prefixes are stripped ("Safeway Pharmacy" โ†’ "Pharmacy"). Unit-tested + device-verified on a 5-department grocery store.
  • โœ… Tabbed place sheet (Google-style): Reviews (rating summary + featured highlight + full list) and About. Layout order: photos (hero) โ†’ info โ†’ hours โ†’ action row โ†’ popular times โ†’ tabs (photos lead so they're visible at the peek height; popular times sit below the action buttons, like Google)
  • โœ… About tab = business description + attributes - leads with Google's editorial one-liner ("Welcoming coffeehouse with handcrafted coffeeโ€ฆ", node [32][1][1]), then the business's own "From the owner" blurb ([154][0][0]), then the attribute sections (Service options, Highlights, Accessibility, โ€ฆ). The description comes before the rest, per request. All three rich fields are trimmed from the keyless/list response, so they ride a lazy WebView detail fetch (PlaceDetails) - wired on every open path (search-result tap, recents/saved, and POI taps; the latter two were missing the call, so popular times + the summary silently never loaded there - fixed 2026-06-20). That same fetch now also backfills the fields a summary node drops (2026-06-21): searching a suite / multi-tenant address snaps to the business via a lightweight summary node that omits the review count, full weekly hours, address, phone, website, price, attributes (e.g. a nail salon at a Ste A-2 address showed 4.4 with no count and only today's hours). The focused name+address re-fetch is a FULL place node, so PopularTimesParser lifts those off too and MapViewModel merges them into any field the summary left blank - feature-id-gated (only from the matching result, so a neighbor's rating/hours can't be grafted on) and purely additive (no fetch/match โ†’ unchanged, no regression). (reviews themselves always loaded via the reviews RPC; this restores the count + the rest of the card.)
  • โœ… Action link - Book / Reserve / Order online (2026-06-21) - Google's primary action button (a salon's "Book online", a restaurant's "Reserve a table" / "Order online") surfaces as a prominent tinted button on the place sheet that opens the provider link; the label adapts per business type. Parsed from [1][75][0][0][5] (label [0], URL [1][2][0], calibratable as actionLabel/actionUrl) and defensively gated - no real http(s) URL โ†’ no button, so a shape change can't render a broken action. (structure verified against a real "Book online" node + unit-tested; on-device visual check pending an unlock; reserve/order coverage for restaurants wants one restaurant capture to confirm the same slot.)
  • โœ… Attribute highlight chips (2026-06-21) - the most-scanned attributes (service options, offerings, accessibility, โ€ฆ) surface as a horizontal chip row on the place overview, Google-style, instead of being buried in the About tab; pulled from the already-parsed About sections (priority-ordered, deduped, capped at 6) so every place with attribute data benefits, including the ones the summary-node enrichment now fills in. (compiled + reuses the verified card-row pattern; on-device visual check pending an unlock.)
  • โœ… "People also search for" (2026-06-21) - a place opened from a focused name search (a business searched by its full name) shows a Google-style horizontal row of related-place cards (name + rating); tap one to open it as a full place (its own reviews/hours/popular-times then load). Lifted from the search response's root[2][11][0] (SearchParser.parseSimilarPlaces, calibratable as similar), each entry [featureId, name, [[_,_,lat,lng], โ€ฆ, rating@6]], attached to the primary result. (Present only when the query focuses on one result - Google's own behavior; multi-result list taps and bare-address snaps don't carry it. Device-verified: 8 related salons on a nail salon's page, tapping one opened it with full details.)
  • โœ… "Also at this location" - when other Google listings sit at the same spot (a co-branded shop's duplicate profile, a different unit at the address), the place sheet lists them with rating + category, tap to open - like Google's co-located-businesses section. Drawn for free from search results already in hand (no extra request). Matches on the same street line (e.g. "239 G St", suite-insensitive), not raw proximity, so a shop across the street isn't wrongly listed
  • โœ… Spoken and typed commands (discussion #365, 2026-09-13): "take me home", "get me to work", "navigate to the station", "Davis to San Francisco", "nearest pharmacy" and "what is my ETA" are understood as actions in all 15 app languages (space-free matching for Chinese and Japanese, verb-after-place for Japanese), with English as the fallback everywhere; anything unrecognized is a plain search. Dictation slips in the command words are tolerated ("navigat to", "nearst pharmacy", "my ofice", a dropped accent in French) without ever touching the place name itself. On-device rules, no server.
  • โœ… More of the Google scrape is remotely repairable (2026-09-13): the directions response field paths and the review scrape's words and CSS hooks now ride the signed calibration bundle, so those drifts are a config edit rather than an app release.
  • โœ… Portuguese (Brazil) (2026-09-28): the app UI in Brazilian Portuguese (values-pt-rBR), contributed by Netocon; the in-app picker lists it as "Portuguรชs (Brasil)" beside "Portuguรชs (Portugal)", and a Brazilian phone that turns off "Follow system language" starts on it. Spoken directions, place content and dictation use the shared Portuguese tables.
  • โœ… Hungarian (2026-09-13): UI, spoken turn-by-turn, open/closed status words, review and transit words, and the Anna voice; contributed by Zsolt Laszlo Kaiser via the kaiser-app fork.
  • โœ… The road pill and the banner shield follow a road that changes its name without a turn (2026-09-13): a rename the voice stays silent about is kept on the leg, so the current-road label updates a mile on instead of sticking to the name the turn entered.
  • โœ… The picture-in-picture mini map wears the turn card (2026-09-13): green banner with the glyph, a bold distance and the road or turn text, instead of a one-line dark caption; no compass in the window, the window ignores taps as map gestures, the drive re-centers when you come back, and on Android 13+ the PiP menu can grow the window taller.
  • โœ… Only the stop signs and lights on your route draw while you drive (user 2026-09-18): navigation replaces the map-wide set with the ones within 120 m of the route, but a map refresh that started as the camera swung into the drive was finishing a moment later and painting the whole neighborhood back over it: 28 controls on the route became 99 across several blocks. The refresh now checks again, after its wait, whether the drive owns the layer.
  • โœ… Mute and pause share one control (user 2026-09-18): the two hold-something buttons of a drive sit in a single pill on the right edge, pause on top where the thumb lands, the way the zoom buttons already pair up. Five stacked buttons was most of a small phone's screen, and worse in landscape.
  • โœ… Pause the drive (user 2026-09-18): pull into a station you just spotted and the app stops arguing with you. Pause keeps the route, the stops and the figures exactly as they are, and stops everything that reacts to where you go: no rerouting, no "make a U-turn", no voice over your music, no faster-route offers. The arrow still follows you, and the arrival time keeps sliding, because what the stop is costing you is worth seeing. Resume picks the drive back up, rerouting from where you are if the stop took you off the route, and driving away resumes it by itself once you are moving and back on the line. On the map and in the notification, since the decision to pull in is made from behind the wheel. Google Maps still cannot do this.
  • โœ… A CI check that catches a location leak before it is public (2026-09-18): every push and pull request is scanned for a private list of place names kept in a repository secret, not in the repo, because a list of terms never to commit is itself the thing you must not commit. It reports the file and the line, never the matched word, and it travels with the repository rather than with one laptop's git hooks.
  • โœ… Wider streets, and street names a zoom earlier (user 2026-09-18): residential streets draw wider through the mid zooms where the map is read most (about 7dp at z16 against 5.9 before, casing to match), and minor street names start at z14 instead of z15. Vela's zoom number reads about one lower than Google's for the same visible area, so a label gated at 15 was arriving a level late next to theirs; street labels are also a point larger from z16.
  • โœ… Google-mode places rank by what kind of place they are, not just review count (user 2026-09-18): Google returns no ranking of its own (its answer is a merge of 15 per-category searches), so review count used to decide everything and a busy taco window could take the label off the hospital behind it. The same category scale the open-data bake uses now shifts the ranking: hospitals, supermarkets, malls, campuses and airports up, places with no category at all down, an ordinary shop exactly where it was, so the two places sources finally agree about what a plaza's anchor is.
  • โœ… The book (2026-09-18): docs/book/ explains Vela subsystem by subsystem with the real numbers in it - which dataset a pin came from and what made it win the label, when every hosted dataset is rebuilt and how your phone picks up a new build, what counts as a camera on your route and what the avoid rule will and will not trade. Three chapters to start (places, data and rebakes, cameras), a stated shape for the rest, and a rule that a behavior change updates its chapter in the same commit.
  • โœ… The places-source rows say what each mode costs you (2026-09-18): the Vela data row now says what decides which places lead (the kind of place first: hospitals, supermarkets, malls and campuses, then brand and detail), the Google row says its own ranking is review count, so the busiest name in a plaza takes the label even when it is not the anchor store, and the Both row warns that Google's extras land a beat after the map and can take a label as they arrive.
  • โœ… Tapping a shop opens the shop, not the bus stop beside it (user 2026-09-18): two separate things could hand your tap to a transit stop. A stop icon anywhere in the tap's reach used to outrank every business in it, so a fuel station on a corner opened the departures board however dead-on the tap was; the stop competes by distance now, like everything else. And the lookup itself could answer with a stop: Google lists stops and intersections as places, they sit meters away on the same corner, and when nothing in Google's answer matched the tapped name the app took the nearest thing it found. A tap on a business now never resolves into a stop or a junction, and a listing that does not match the name has to be on the same lot, or the tapped place simply keeps its own name and details.
  • โœ… The map holds still while you look at it (user 2026-09-18): a settled view gets painted several times (the place fan-out streams its answers, the de-duplication pass re-runs, and a cold session refetches everything once because Google strips review counts for the first few seconds), and each paint used to re-rank every icon. Labels traded places, icons grew and shrank, and a sushi counter could take the label off the supermarket it sits inside, twenty seconds after the map already looked right. The first proper paint of a view now fixes the order; later answers can still ADD places, but they cannot reshuffle or resize what is already on screen. Moving the map ranks it fresh.
  • โœ… The place sheet stops moving under your thumb (user 2026-09-18): the photo strip and the rating line used to arrive a second or two after a place opened and shove Directions, Start and Street View about 88dp down the screen, so a tap aimed at Directions landed on something else. Both hold their space from the first frame for a place that will have them, and fill in place.
  • โœ… The route chooser shows the trip's own pins, and the end of the trip wears a flag (user 2026-09-17): while you are picking a route the other search results drop off the map, so only what the trip is made of is drawn - your stops in visit order and the destination, which now has its own teal flag pin instead of a red search pin, so the last point reads as the end and not as one more stop.
  • โœ… The route chooser fits landscape (user 2026-09-17): in landscape the panel is capped to the room under the endpoints card and tightens up - the mode tabs move into the header row beside the options, share and close buttons, the paddings halve, and the middle scrolls - so it stops covering the stops card and Start stays on screen. Rotating the phone also switches the layout properly now: with the Activity handling rotation itself, every screen-size read was stuck on the previous orientation until the app was restarted, so landscape kept the portrait chrome (and back again).
  • โœ… The route stops flashing blue (user 2026-09-17): the part of the route you have driven turns gray, and every route swap during a drive (a recheck that heals the line, a reroute) briefly dropped that fraction to zero, so the whole line flashed blue for a frame. The fraction is held across the gap now, and released the moment the line itself changes.
  • โœ… Start stays visible in the route chooser (2026-09-13): the route list scrolls under a fixed Start and Steps row instead of pushing them off the bottom when there are several alternates.
  • โœ… A typed search inside the origin, destination or stop picker shows its results (issue #405, 2026-09-13): searching "Coffee" from Add stop and pressing Enter used to show nothing; the results sheet now appears inside the picker and a tap adds the place.
  • โœ… Trips record each fix's provider, the off-route count, and every nav decision (2026-09-13): which provider a fix came from, how far the reroute debounce had counted, and a line for every recheck offered or kept (with the candidate's saving and why), faster route accepted or dismissed, and reroute attempt; the audit prints them.
  • โœ… Trip exports are named by date and time, and record which kind of route was driven (2026-09-13): files come out as vela-trip-2026-09-13-1432.csv instead of an opaque id, and each route block in the file now says whether it was a provisional Google alternate, abbreviated, offline or traffic-aware and how many steps it had, so a bad swap shows in the file itself.
  • โœ… The full review page retries when Google withholds the feed (2026-09-13): a reload, then a reload on a fresh session, before it gives up with a message; a healthy session's "More reviews" button was verified to load the full list.
  • โœ… A reroute with a downloaded region under it answers from the phone first (2026-09-28): when the region covers both ends of the trip, the on-device route is computed at the same time as the online one and used if the online router has not answered within 2.5 seconds; the online route with its traffic replaces it a little later through the usual recheck, or is offered as a faster route if it takes another course. Trips with stops do the same.
  • โœ… Transit directions without Google (2026-09-28): with "Use Vela without Google" on, or when Google's transit page returns nothing, the route chooser asks Transitous' own planner and shows its itineraries the same way, including the mode-chip time. Google stays first because its times account for traffic and history.
  • โœ… A reroute no longer waits out a stalled open router (issues #557 and #258, 2026-09-17): a reroute gives the open router 6 seconds (the escalated attempt 8 seconds a try, inside about half its time) and then takes Google's route from the same request, or computes one on the phone from a downloaded region, whichever answers first, keeping the avoid settings and, offline too, the direction the car is heading. A second off-route moment right after a new route used to go unanswered until the driver was back on the line; it now retries as soon as the short cooldown ends. Diagnostics say which source answered, how long each step took, and when nav was ended with a reroute still running.
  • โœ… A faster route or a reroute no longer arrives with its turns guessed (2026-09-13, from a shared trip): when Google's alternate led the reply it was driven raw, with one maneuver placed at the car's position and announced at 30 feet; the session now names such a route through the open router first, like the picker does.
  • โœ… The full-screen review page follows the app language (issue #359, 2026-09-13): it used to be pinned to English because the carve keyed on English labels; every hook now reads the per-language word tables, the histogram is parsed by its leading digit, and the sort menu is clicked by position. Verified in Traditional Chinese and English.
  • โœ… What's new after an update (2026-09-13): the first launch on a new version shows that version's release notes once, in the same dialog as the donate prompt, with a Full notes link; Settings > About reopens it. Never on a fresh install, never without the notes.
  • โœ… The full review page opens on the reviews for places whose name contains a review word (issue #535, 2026-09-16): "Davis" contains the French "avis", so the page took the Overview tab for the Reviews tab, stayed on the Overview, and its More reviews button reloaded back to it. Tab and button labels are tested with the place name cut out, on the full page and the place-sheet scrape. The full page now writes to the Diagnostics export too (opened, loaded, retries, the More reviews tap with review counts before and after). NB Google now shows signed-out visitors "a limited view", so any review feed ends after a limited number of reviews.
  • โœ… Reviews load again on the place sheet, and the full-screen review page scrolls both ways (issue #359, 2026-09-13): the inline scrape had been broken by a script error since September 6, and the full-screen page closed itself when you scrolled back up.
  • โœ… Traffic lights, stop signs, crossings, humps and speed cameras come from baked per-region files (issue #304, 2026-09-13): built on CI from OpenStreetMap extracts and downloaded once per region, instead of a live Overpass query per viewport and per drive. Overpass remains only where no region file exists.
  • โœ… Saving an area no longer queries Overpass (issue #304, 2026-09-13): the places, addresses and streets for a saved area come from the region's pre-built place pack, which the same save downloads; the live Overpass fetch remains only for an area no pack covers.
  • โœ… Search leads with what is near you and can page further (2026-09-13): a category search from where you stand adds a pass over the few blocks around you so the outlet next door makes the list, and a "More results" row at the end of the list pulls the next pages on demand.
  • โœ… House-number zoom is a setting (issue #329, 2026-09-13): Settings > Map > House numbers picks how far out the numbers appear, with the default a step further out than before.
  • โœ… The first GPS fix no longer yanks a map you have already panned (issue #362, 2026-09-13).
  • โœ… OpenStreetMap attribution (issue #302, 2026-09-12; shortened 2026-09-18): a tappable "ยฉ OpenStreetMap" credit sits bottom-left of the map in every state, browse and nav, and Settings > About has a Map data section saying where the map, the places and the imagery come from. The OSM Foundation's attribution guidelines accept that short form alongside the longer one, as long as the credit leads to the license, which the tap-through does.
  • โœ… Directions panel (Google-style popup, not buried in the place sheet): tapping Directions opens the route chooser. The From โ†’ To header is a card at the TOP of the screen now (2026-07-13, Google's layout): the search bar swaps for an endpoints card - origin ring, connector dots, red destination pin, stops between, back arrow and a swap (โ‡„) button to reverse the route (you โ‡„ the place) - so the endpoints stay visible and editable even with the chooser collapsed to its Start bar. The bottom panel keeps Drive / Transit / Walk / Bike tabs, and a prominent Start + Steps. The mode chips show each mode's time (2026-09-12): once a mode's route is known the chip reads "25 min" under its glyph, as Google's do; the current mode's time is the picker's fastest option and the other three are fetched in the background through the same routing calls, so the chip and the list never disagree. Cached per trip, so flipping between modes is instant. For drive/walk/bike it lists the route options - each with a traffic-colored ETA (green free-flowing โ†’ amber โ†’ red), distance, via-road name, a "Fastest" tag on the best, and a "+N min" delta on each slower alternate (2026-07-01) so you can weigh them at a glance the way Google does; tap an alternate to select it (the map line switches to it). Transit shows the results board instead. Collapsing the chooser to its Start bar re-frames the route closer (2026-07-14): the camera fit re-runs with the freed screen, so minimizing is how you get the big route view instead of the fit staying zoomed out for a panel that's no longer there. And map taps during a live drive are inert (2026-07-14): only a pick from the in-nav search results becomes a stop; a stray tap on a POI dot can't silently pin itself onto the route anymore.
  • โœ… Route from a different starting point (not just your location) - the directions panel's From row is tappable (pencil affordance): pick any place via search and the route recomputes from there, like Google. Mirrors the existing To + โ‡„ swap; the search overlay covers the panel while picking and restores it on choose/cancel. Editable in both directions - the pencil sits on the "From" row normally, and moves to the "To" row when the route is reversed (that's where the custom endpoint then lives). A "Your location" reset row drops a custom endpoint back to live GPS.
  • โœ… Avoid tolls / avoid highways (LIVE 2026-07-11; avoid ferries added 2026-09-16) - sticky chips in the route chooser (drive). All 135 region graphs carry the avoid profiles (v2 rebake) and the app reads the v2 manifest, so any downloaded region routes around tolls and motorways on-device; verified on a Delaware toll road. The public OSRM rejects exclude, so an online-only route (no graph downloaded) still falls back to a normal route
  • โœ… Day and night map theme (2026-08-15, issue #262) - a "Day and night" appearance mode that is light by day and dark after sunset, worked out on the device from your location with the standard sunrise equation: no network, no key, no almanac. It is not the same as following the system theme, which is only ever whatever the phone is set to. Because sunset is a question about the sun and not the clock, it is right in December and in June, at the equator and inside the Arctic circle (where there is no sunrise at all for weeks and the answer still has to be day or night). There is also a separate "day and night while navigating" switch for people who keep the app dark by choice but do not want a dark map in daylight while they are actually driving by it. The stored position is rounded to about a kilometer, which is all sunset needs.
  • โœ… Satellite view + a layers panel (2026-07-13/14) - a map-corner Layers button (optional, Settings toggle) opens a Google-style panel: Satellite view (open Esri World Imagery tiles, keyless, attribution shown with the area's capture year under it), plus the live traffic, transit-lines and terrain overlays in one place. (2026-09-29, #626) The panel also picks where the map's places come from (Vela data, Google or both) and holds the "Use Vela without Google" switch, the same settings as Settings > Places and Privacy, one tap from the map; Settings > Map can hide the parking button while no spot is saved. Deep zoom (2026-08-06, issue #244): the base imagery is capped at z19, so closer zooms used to stretch the last tile into blur; the app now probes Esri's own per-area availability index and serves its native z20+ tiles where they exist, falling back to Google's imagery tiles for the deep zooms only where Esri tops out at 19. Esri stays the primary source everywhere it has data. Satellite mode is a real hybrid: translucent ghost roads keep the network readable under tree cover (freeways tinted yellow like the Google app), every map label flips to white with a black halo, and the POI teardrops go white so icons read over imagery. Overlays layer correctly above the photo; toggling back restores the normal palette cleanly.
  • โœ… Numbered stop pins on the map (2026-07-14) - a trip's intermediate stops draw as teal pins numbered in visit order while the chooser is open or the drive is running; reordering stops in the editor re-numbers the pins immediately.
  • โœ… Multi-stop directions (waypoints) (2026-07-01) - an "Add stop" row between From and To (Google-style) opens the same place picker; the pick becomes an intermediate stop and the route recomputes straight through it, with each stop removable (ร—). Routes OSRM through origin โ†’ stopsโ€ฆ โ†’ destination (routeVia, which filters the spurious per-via arrive/depart into one continuous trip) and overlays Google's live in-traffic ETA ratio; a waypointed trip is a single route (no alternates). Stops reverse with โ‡„. Device-verified (Davisโ†’Sacramento: adding a mall stop rerouted the trip a few minutes longer via a through-path, remove reverted to the direct route + alternates). Stops editor 2026-07-08 (user: the inline arrow/X cram was ambiguous): the panel header now shows ONE compact tappable summary of the stops; tapping it (or the pencil) opens a dedicated stops editor sheet (StopsEditorSheet) - origin at top, each stop a row with a drag handle (drag past a neighbor's midpoint to swap), numbered dot, and X, destination pinned last, Add stop + Done under the list. Edits are local until Done applies them in ONE reroute (applyStops); back/X discards. Under D-pad the rows show up/down arrows instead of the (undraggable-by-key) handle. Glyph grammar (2026-07-08): the origin wears a location-blue pin when it's your current position (the non-verbal "this is you"), the destination a checkered flag - in the directions header AND the editor; header rows share a fixed-width icon rail so the glyphs align, and the stop summary shows the first stop plus a +N badge. Mid-drive (2026-09-14, #402): the nav step sheet leads with a Stops row naming the stops still ahead; Edit stops opens the same editor over the ETA bar (origin = where you are), and Done replans the drive once through the new order, dropped stops gone, added ones in. Drag fix (2026-09-16): dragging a stop by its handle froze after the first move and never reordered anything; it follows the finger and swaps now. Quicker edits (2026-09-25, #604/#607): the step list shows Edit route on every drive, stops or not, so adding the first stop mid-drive is one tap away; with stops ahead there is a Remove next button (asks first), and tapping a place that is already a stop offers Remove stop beside Add stop. Add stop mid-drive (2026-09-29, #623): the editor's Add stop opens the search along the route; it used to close the editor with no search and leave Edit stops dead until Back. Stops no longer drop out of the list right after an edit. Search along the route during a drive skips places already passed, and each result's distance is roughly the drive to it (it read "10 ft" for places beside the car).
  • โœ… Road name inside the nav bar (2026-09-16, issue #553). Settings > Navigation > Current road name has a fourth choice, Inside the bottom bar: the name of the road you are on sits in the bar's top row instead of floating above it, so it never covers the arrow.
  • โœ… Calmer dense cities and a cleaner route overview (2026-09-16). In a crowded downtown, office tenants and small practices show as dots unless they are among the top few on their block, every block has an icon limit even at the closest zoom, and a name never shows without its icon. In Midtown Manhattan this took panning from about 29 to 46 frames per second at street zoom. While you choose a route, the map shows only major landmarks.
  • โœ… Drive navigation map cleanup (2026-09-16). While driving, the Vela places layer shows only gas stations, like the rest of the map, so business labels no longer sit on the road. Street-name bubbles now appear where each cross street meets your route instead of somewhere along that street. Traffic lights and stop signs near you draw above the blue line. The mini map (picture-in-picture) re-centers on your arrow when it opens and when you come back. Driving without a route, the map turns and looks ahead smoothly instead of in small steps once a second.
  • โœ… Downloaded regions update themselves on Wi-Fi (2026-09-25). Settings > Offline maps > "Update downloaded regions" now defaults to On Wi-Fi: a minute after start, at most once a day, Vela applies any small published patch to the regions you downloaded, on an unmetered network, and never downloads a whole region by itself. If you had picked "Never on its own" it stays that way.
  • โœ… Popular times speak your language (2026-09-25). The "Not busy / A little busy / Very busy right now" line under popular times was English in every language; it is translated into all 15 now.
  • โœ… The picture-in-picture mini map shows the time left and the arrival time (2026-09-25), along the bottom like Google's.
  • โœ… Street-name bubbles no longer cover surveillance or speed cameras during a drive (2026-09-25). The camera badges are placed first and the bubbles move out of their way.
  • โœ… Reviews load when you get to them (2026-09-25). The review list comes from a Google page that costs about 137 requests to load, and most taps on a place never scroll down to it, so it now loads when the Reviews tab comes into view instead of on every tap. "Load all photos and reviews" (Settings > Performance) keeps the old behavior.
  • โœ… A downloaded map stays up on a connection that doesn't actually reach the internet (2026-09-26). A car stereo on a car Wi-Fi or a phone hotspot with no data looked online, so Vela dropped the downloaded map for streaming that couldn't load and showed gray.
  • โœ… Updating a downloaded region shows its progress in Settings whatever the update holds (2026-09-25). An update that was only the places or the map used to leave the Update button sitting there with no sign anything was happening.
  • โœ… Light map, dark app, or the reverse (2026-09-28). Settings > Appearance > Map picks the map's own light or dark apart from the app's theme.
  • โœ… The nav voice follows the guidance language (2026-09-28). With a voice for that language installed, switching the phone or app language mid-session switches the spoken directions to it; without one, the heads-up offers a one-tap download of the language's recommended voice.
  • โœ… Heads-up cards dismiss themselves (2026-09-28). Informational cards (area saved, region downloaded) drain a bar over ten seconds and go; a card with a fix to tap stays.
  • โœ… Downloaded pieces update (2026-09-28). A newer bake of a downloaded piece shows on its row with Update, and the automatic updates setting pulls it.
  • โœ… Download just part of a region (2026-09-28). Where a region is baked as grid cells, the area picker offers offline directions and places for the framed area alone (a few small pieces instead of the whole state), one download under the usual card, listed and deleted per region under Offline maps > Downloaded; neighboring pieces draw as one map and downloading the whole region afterwards replaces them. Delaware is the first region published this way.
  • โœ… Pick the area to download on the map, like Google Maps (2026-09-25, issue #609). Settings > Offline maps > "Download an area" opens the map with a frame over it: move and zoom until the frame covers what you want, from a neighborhood to about half a state, and the card under it shows the size as you go. Everything in the frame is saved at full street detail. The whole region around it (offline directions, places and the region's map) is a separate checkbox, since those only come as a whole region; they used to download silently.
  • โœ… Requests to Google, counted (2026-09-25). Settings > Privacy shows how many requests Vela sent to Google today and over the last week, by purpose (searches, place details, photos, images, the pages Vela opens in the background and everything those pages load). Counted on the phone, kept 14 days, never sent anywhere; the diagnostics export carries the counts too. Map tiles for live traffic and some satellite imagery are not included.
  • โœ… Place photos are fetched like a browser fetches them (2026-09-25). The image loader had its own plain HTTP client and asked Google for every photo as "okhttp"; photos now go over the same Chrome network stack as everything else, with Chrome's image headers.
  • โœ… Block Google's page telemetry is a setting (2026-09-25). Settings > Privacy. Google's pages report on themselves in the background (logging pings and the account bar's calls). Nothing Vela shows needs them, but they go out by default, because a browser that never sends them looks less like a person to Google and that can land your session in the limited view. Turn it on to answer them on the phone instead.
  • โœ… Vela says when Google is holding back (2026-09-25). Google sometimes gives a phone's session a "limited view": fewer photos, no popular times on some places, and a More reviews button that loads nothing. Vela used to just look broken when that happened. Now it notices (the photo request comes back with 10 photos instead of the 50 asked for, or More reviews loads nothing), and says so in one quiet line where the popular times would be, and in Settings > Privacy > Google session. It clears itself when Google gives full answers again.
  • โœ… Street-name bubbles stay until you pass them, then fade (2026-09-25). They used to vanish a few meters before you reached the street, sometimes two seconds late, and always in one frame. Now a passed bubble rides down the screen with the map and fades out over about a second just before it reaches the bottom bar, with no flicker.
  • โœ… The same category chips everywhere (2026-09-16). The map, the route chooser's search along route and the search while navigating show one set of chips in one order: Restaurants, Coffee, Gas, EV charging, Groceries, Hotels, Pharmacy, ATMs and Parks. The route and navigation rows used to offer a shorter, differently ordered set with "Food" in place of Restaurants.
  • โœ… More category chips (2026-09-16, #554). The row now matches Google's common set: Restaurants, Coffee, Gas, Groceries, Things to do, Hotels, Bars, EV charging, Parking, Pharmacy, ATMs, Parks, Hospitals, Banks, Post offices and Campgrounds. Bars is left out while "Hide adult categories" is on, since that filter would empty it. Every chip works offline too: the place search now understands the plural words (Hotels, ATMs and Parks used to find nothing offline), and "Things to do" looks for attractions, museums, viewpoints, zoos, theme parks, galleries, theatres and cinemas.
  • โœ… Multi-stop directions (waypoints) (2026-07-01) - an "Add stop" row between From and To (Google-style) opens the same place picker; the pick becomes an intermediate stop and the route recomputes straight through it, with each stop removable (ร—). Routes OSRM through origin โ†’ stopsโ€ฆ โ†’ destination (routeVia, which filters the spurious per-via arrive/depart into one continuous trip) and overlays Google's live in-traffic ETA ratio; a waypointed trip is a single route (no alternates). Stops reverse with โ‡„. Device-verified (Davisโ†’Sacramento: adding a mall stop rerouted the trip a few minutes longer via a through-path, remove reverted to the direct route + alternates). Stops editor 2026-07-08 (user: the inline arrow/X cram was ambiguous): the panel header now shows ONE compact tappable summary of the stops; tapping it (or the pencil) opens a dedicated stops editor sheet (StopsEditorSheet) - origin at top, each stop a row with a drag handle (drag past a neighbor's midpoint to swap), numbered dot, and X, destination pinned last, Add stop + Done under the list. Edits are local until Done applies them in ONE reroute (applyStops); back/X discards. Under D-pad the rows show up/down arrows instead of the (undraggable-by-key) handle. Glyph grammar (2026-07-08): the origin wears a location-blue pin when it's your current position (the non-verbal "this is you"), the destination a checkered flag - in the directions header AND the editor; header rows share a fixed-width icon rail so the glyphs align, and the stop summary shows the first stop plus a +N badge. Mid-drive (2026-09-14, #402): the nav step sheet leads with a Stops row naming the stops still ahead; Edit stops opens the same editor over the ETA bar (origin = where you are), and Done replans the drive once through the new order, dropped stops gone, added ones in.
  • โœ… Google-style route picker (2026-09-16; the DEFAULT since 2026-09-18). Tapping Directions opens a picker laid out like Google Maps' 2026 one, and Settings > Navigation > "Google-style route picker" turns it off for Vela's classic panel, which lists every route at once: a "Drive" header with round options/share/close buttons, underlined mode tabs with times, ONE route summary (time in its traffic color, distance, "Fastest route now" or "N min slower", via road), time bubbles on the map for every route (the selected one blue, the others white; tap a bubble to pick that route), and a sticky Start / Add stops / Share bar. Dragging the sheet up shows the time and avoid chips, "Add a stop along the way", and the turn list inline. The endpoints card gains a โ‹ฎ menu (Edit stops, Add stop). Edit stops is a full-trip editor (issue #516): every row, the start and the destination included, can be dragged to any position or removed, and Done reroutes once, so the start can be moved down the list or dropped. Transit keeps the regular chooser. The whole fleet can be put back on the classic panel from the signed calibration bundle (classicRoutePicker) if the new default ever goes wrong; your own toggle always wins.
  • โœ… Long-press to route through a point (2026-07-12). While a route is being planned (the directions panel is up), long-pressing any spot on the map adds it as a via-stop and the route reroutes straight through it, with a "Stop added" confirmation. It's the quick, Google-like way to grab an arbitrary point - no search, no crosshair - and it's the practical way to steer a route around an area (a construction zone, a Flock-camera stretch): Google's keyless directions and OSRM can't be told "avoid this region", but a hand-placed waypoint forces the detour. The stop sits at the exact point pressed (the reverse-geocode only names it), reuses the existing multi-stop pipeline (removable/reorderable in the stops editor), and only fires while planning so a normal long-press still drops an inspect pin. Device-verified (a long-press mid-route added a trailhead as a stop and the ETA/line rerouted through it). Localized in all supported languages.
  • โœ… Multi-stop follow-ups (2026-07-01): (1) per-stop arrival cue - nav announces "You've reached <stop>" as you pass each waypoint (NavEngine.stopMarks projects each stop onto the route line โ†’ an along-route meter "mark"; NavSession fires the voice cue in order as progress passes it - unit-tested, NavEngine untouched); (2) reroute-through-remaining - an off-route reroute (and the faster-route recheck) now route through the stops you haven't reached yet, instead of straight to the final destination (which used to silently drop them); the reaches-dest nav-safety guards are unchanged (the route still ends at the same dest). (3) Reorder - up/down arrows on each stop row (with 2+ stops) reorder them and re-route (moveStop).
  • โœ… Depart / arrive time (+ date, + Last available) - one Leave now / Depart at / Arrive by chooser at the top of the directions panel (a Last available chip too in transit), applying to all modes, with both a time field AND a date field (plan any upcoming day, Google-style). Picking a time re-fetches transit for that moment: Google's board is time-dependent, so WebDirectionsFetcher inserts Google's time block into the /maps/dir/โ€ฆ data param - !4m6!4m5!2m3!6e{0=depart,1=arrive,2=last}!7e2!8j<unix-seconds>!3e3 (the !4m wrappers are descendant counts, verified against a real Google transit-with-time URL; an earlier !4m8!4m7 guess had the wrong counts and Google silently fell back to "now"). For drive it also shows the honest arrival/leave window from Google's own typical bestโ†’worst spread (summary[10][4] โ†’ Route.typicalRangeSeconds, e.g. "arrive 6:08โ€“6:27 PM ยท in typical traffic"). Per-minute future-traffic prediction on drive stays login/Android-app-only, so drive surfaces the spread Google itself plans with rather than a false-precision single time
  • โœ… Search along route - with a trip planned, the directions panel shows Gas / Food / Coffee / Groceries chips (the fuel chip reads and searches "Petrol" where the phone's region says so, issue #338, 2026-09-06); tapping one searches near the route and shows only the results within ~3 km of the route line, ordered start โ†’ destination (so you find stops actually on the way). The route stays drawn. Overhauled 2026-07-08: each result now shows its distance along the trip (meters from the route start to where it sits), not the old crow-flies distance from the route midpoint (two stations at opposite ends both read "5.9 mi"); tapping a result adds it as a STOP on the trip and returns to the directions panel with the destination intact (it used to open the place's own sheet, whose Directions button silently replaced the whole trip); picking the destination itself or closing the results (X) just returns to the panel. The trip's destination is stashed in alongRouteDest while browsing. Works DURING nav too (2026-07-13): a search button floats on the nav map's right edge (with the volume toggle - the bottom bar keeps just ETA, Steps and End) and opens the chips PLUS a free-text field for arbitrary searches above the bar; a pick searches the remaining route, the results list takes the bottom slot (BACK peels it, never ends the drive), and tapping a result inserts it as the NEXT stop on the live drive (NavSession.addStop replans through it, voice says rerouting, remaining stops keep their order).
  • โœ… Consistent sheet styling - the place sheet, directions panel, route chooser, steps list, nav bar, the search-results list, the full-screen search page, and the "set as Home/Work" banner now all share one Google-gray palette (ui/SheetPalette: #1F1F1F/#FFF surface, fixed ink/dim text, teal accent, green/amber/red traffic) instead of differently-shaded Material-You-tinted cards
  • โœ… Permanently-closed (dead) POIs - detected from the place node's [23]==1 flag (the reliable signal - a dead POI like Caffรฉ Italia carries no open/closed status text at all, so the earlier text-only check missed it; the flag survives the keyless degraded response, calibrated live 2026-06-19), plus a "Permanently" text fallback. They stay in search results (labeled "Permanently closed" in red, in both the row and the place sheet) but are dropped from the map pins so they don't clutter the map (a place you explicitly open still gets its pin)
  • โœ… Alternate routes - Google's 2-3 driving alternates are surfaced + selectable in the directions panel (e.g. "20 min ยท via I-80 E" / "21 min ยท via Co Hwy E6"); each draws along Google's OWN route geometry (delta-encoded in the response at [0][7][i], decoded directly) so the line matches the via-label exactly, alternates included - no more lines that double back on themselves or cut straight across (the old scattered-point guess is gone; an open router is now only a fallback for routes Google omits geometry for)
  • โœ… Alternates drawn grayed on the map + tappable (Google-style) - the non-selected routes render as gray lines beneath the active blue one (theme-aware shade so they read on dark/light tiles); tap a gray alternate to switch to it (same as picking it in the panel). The directions camera now also frames the whole route above the panel (per-edge bottom padding) instead of centring it behind the card (rendering + framing verified on-device, Davisโ†’South Lake Tahoe: gray I-80 arc alongside the blue US-50 route). The route line is drawn below the basemap's label layers (Google-style), so road names and POI text stay legible on top of it instead of being painted over (2026-06-21)
  • โœ… Place sheet peeks (~56% screen) so the business info isn't immediately full-screen and the map stays visible above it; drag the handle up to expand (~92%, for the reviews), down to shrink, down again to dismiss. The body scrolls, so a tall place (hours + tabs) is fully reachable at either height
  • ๐ŸŸก Open places layer (2026-09-14, issue #441). Settings, Data & privacy (moved from Map 2026-09-16), "Places come from" (Vela data, the default, the open layer alone, offline and quiet; Both, the open layer plus one Google request once the view settles, overlap dropped by name and distance; Google, the old fan-out on every pan with its costs spelled out; the fleet default is set on the signed calibration channel since 2026-09-16, so it can change without an app release), with a Learn more dialog on who maintains the data, where Vela serves it from, and what still goes to Google. Landmarks (airports, hospitals, universities, malls) draw from z11, the way Google keeps them zoomed out. Dots draw under every label and come in by rank as you zoom (none at z14, a few per block at z15, all from z17), and saved or parking pins claim their spot so nothing is drawn half-covered under them. Settings, Offline maps, "Include places with downloads" (on by default) makes a region download bring its places archives too: the places catalog is cut by state, province and region (German Laender, French and Spanish regions, Brazil's zones), so a whole-country download brings every piece and a province download brings just its own, about half a gigabyte for a big US state. Offline maps also lists one "All of Germany" style row per split country with a Download all button that queues every piece, so the split never costs more taps. A region download now includes the map itself (2026-09-14): the same OpenStreetMap vector tiles the online map draws, baked per region by planetiler from the same extract as the routing file and served from disk with the same style, fonts and icons, so a state or province with no signal shows streets, names, buildings and shields, not just routes and pins. Saarland is 33 MB at full detail; a big US state is a few hundred MB. Only the sample region is baked so far; the workflow bakes the rest. Places and basemap archives rebake monthly from the newest Overture release and OpenStreetMap, and a region whose data has a newer bake shows Update on its row, one tap for the pack, the places, the map and the routing file. With no signal, tapping a place shows what the phone has (the tile's data, or the Google listing remembered from an earlier tap) and never tries Google for reviews, photos or hours. With Open data, where a region file covers the view, the businesses on the map come from open data (Overture Places baked to PMTiles by tools/build-places-region.sh, drawn with the same icons and sizes as the Google dots) instead of the fetch that used to fire as you pan. Density follows a rank baked into the tiles: every place is ranked against its neighbors inside a 400 m and a 1.6 km cell, and at each zoom only the top few per cell get an icon, fewer still a label, and the rest draw as small category-colored dots, so a downtown reads as its landmarks at z15 and fills in with the shops as you zoom, like Google. Google is asked only when you tap a place, through the same name-and-distance correlation a basemap tap uses; offline, the tile's own category, address, phone and website fill the sheet. Regions so far: Davis (test box) and all of California, streamed from the places-overlays release or downloaded along with a region for offline use; only the smallest region covering the view is used, so nested regions never draw twice; a second tap on the same pin is instant. AllThePlaces joins the bake (2026-09-15): every chain's own store locator, scraped weekly by the AllThePlaces project (CC0), is merged into each region's places at bake time, so a chain store with no Facebook page still shows; rows that match an Overture listing by brand or name within 150 m are dropped, and the chain rows carry opening hours, which Overture never has; the offline sheet shows them as a normal hours list with an open/closed status, and OpenStreetMap hours from the place packs get the same treatment. OSM fills the gaps (2026-09-15): the OpenStreetMap businesses on the basemap used to hide entirely under the open layer; now only the ones the open layer already draws step aside, so a museum or shop that exists only in OSM still shows. Parks, schools, civic and transit stay with OSM
  • โœ… The update card lists every release you skipped (issue #330, 2026-09-14) - its What's new covers each release between the version installed and the one offered, newest first, each under its version, instead of only the last one
  • โœ… EV charging chip (issue #435, 2026-09-14) - beside Gas on the map's category row and in the search-along-route chips during a drive. It finds stations; charger availability and plug types are stripped from the keyless data, so the chip does not claim them
  • โœ… Rename a saved place (issue #434, 2026-09-14) - the saved row's menu on the search page has Rename, so a parking lot saved as coordinates or a road name can be called what you call it; the open sheet follows
  • โœ… Prefer bus, subway, train or tram on transit trips (issue #431, 2026-09-14) - chips under the time chooser on the transit tab, carried on Google's request, so a bus-only rider gets the all-bus itinerary instead of the train
  • โœ… Congestion colors on every route (issue #403, 2026-09-14) - the route line used to color by traffic only when it followed Google's course exactly, and never on a trip with stops, so a long trip that differed anywhere was solid blue. Google's congestion is now carried onto every route in the chooser, alternates and multi-stop included, wherever the two share the road; stretches Google did not drive stay plain
  • โœ… A stop sign counts when it is on your road (2026-09-17) - the baked road-features file now carries the orientation of the road each control sits on, and a stop sign is kept only when that road runs the way you are driving. The sign holding the street that enters your road is not yours and no longer draws or counts. An earlier attempt measured distance from the route line instead and binned nearly every sign on a real drive; this one asks what the sign is for. Regions bake the new column over time; until one does, every sign shows as before.
  • โœ… Fixes to yesterday's nav work (2026-09-17) - a cross-street callout whose point could not be measured along the route was treated as already passed and never drawn; a new drive no longer inherits the last drive's progress in that test; and the places shown while driving with tap-to-stop on are capped to the best two per block, which is where the 4a's dropped frames came from.
  • โœ… A download can no longer make the map worse (2026-09-18) - a region whose offline map was baked one zoom level short, which happens when a full bake would pass the 2 GiB limit on a release file, drew as a blurred, detail-less version of the same map at street zoom. Vela now reads each installed archive's real depth and keeps streaming the sharp tiles while you have a connection, falling back to the shallow archive only when you are actually offline, where it still beats an empty screen.
  • โœ… Start always begins where you are (issue #463, 2026-09-17) - planning a trip between two places you are not standing at, then tapping Start, used to begin guidance on a line you were nowhere near; navigation then decided you were off route and re-planned a few seconds later, which read as the app throwing your trip away for an older one. Start now re-plans from your position first, keeping the destination and the stops, and says so. Start at the place you chose (within 150 m) and nothing changes.
  • โœ… The start and the destination are editable too (issue #516, 2026-09-17) - the trip editor that lets you drag or drop ANY row, the start and the destination included, is now what every route chooser opens. It was behind the chooser experiment while the plain editor pinned both ends. Dragging the destination above the start reverses the trip; the last two rows keep their remove buttons disabled, since a trip needs two ends.
  • โœ… Tap a place while driving to add it as a stop (2026-09-17, experimental) - a switch under Settings > Navigation, off by default and marked as an experiment: adding a stop mid-drive re-plans through Google, so the ETA has to re-settle and there is more that can go wrong than with a stop picked before you set off. With it on, fuel, food and charging places stay on the map during a drive, and tapping one offers it in a card above the bar: a second tap on "Add stop" adds it to the trip. A stray touch at speed changes nothing. Because navigation hides places on purpose, turning this on also turns on Show places, and turning it back off undoes exactly that.
  • โœ… Closing-soon warning covers stops (2026-09-25, #606) - the warning that a place closes before (or within an hour of) your arrival now checks every stop on the trip at its own arrival time, not just the destination, and runs again for a stop added during the drive.
  • โœ… Alternate routes live with the ETA (2026-09-17) - the Google-style chooser carries an "N other routes" line right under the time. It opens a list in place, each route with its time, how much longer it is than the fastest, its distance, the roads it uses and its camera count, with the fewest-camera route named as such when camera avoidance is on, and Back returns to the summary. With one route the line says so. While the list is open each route's bubble on the map carries the same detail, distance and the difference, instead of the time alone.
  • โœ… Shops sit where OpenStreetMap says (2026-09-17) - the places bake now reads the region's OpenStreetMap extract and takes OSM's coordinate for a shop whose name matches within about 150 m (widened 2026-09-22 from 30 to 120 m), ahead of the chain locator and ahead of the bulk data's parcel point. OSM maps a shop at the shop, and when it is wrong anyone can fix it in a minute and every map benefits.
  • โœ… The exit you take wears a green callout (2026-09-17) - while navigating, the exit your route leaves at gets a green bubble with its number, placed on the ramp rather than on the freeway beside it, in the language of the signs on the road; the exits you drive past keep the basemap's own shields. The number is read out of the maneuver ("Take exit 528 toward ..."), including the phrasings other languages use.
  • โœ… One camera badge per corner (2026-09-17) - a junction with a plate camera on each approach drew a pile of overlapping badges. It now draws one badge with every head's beam fanning from it and a small "x4" for the count. The route's camera count still counts the real heads.
  • โœ… Street callouts skip the spots with no room (2026-09-17) - a cross-street bubble that cannot clear the road you are driving, on either side, is not drawn at all, and every callout disappears once you are past it instead of trailing behind the car into the bottom bar.
  • โœ… The Overview button is a toggle during a drive (issue #631, 2026-09-30). Pressing it again while the overview shows goes back to following the car (its icon changes to say so); before, a second press re-ran the overview fit, which read as the map jerking and settling.
  • โœ… The route overview shows both ends (2026-09-17) - the camera used a fixed fraction of the screen for the route panel, and the real panel is taller than that, so the start of a long trip sat behind it. The panel now reports its measured top edge and the overview frames the route in what is actually visible.
  • โœ… A shop's own counters stop outranking the shop (2026-09-17) - a supermarket's pharmacy, its coffee bar, its sushi counter, a Redbox, a Coinstar, an ecoATM, a Western Union window and the brand's fuel kiosk out in the lot are baked as parts of the store: they no longer take the block's icon, and they fill in as their own pins only when you are right on top of them. A Sacramento test block had nine of them within 42 m of one Safeway. Chain shops also take their position from the chain's own store locator when it disagrees with the parcel point by more than 30 m, so a chain store pinned out in the parking lot lands on its storefront.
  • โœ… OpenStreetMap shops are on by default (2026-09-17) - in Vela data mode, businesses mapped in OpenStreetMap draw alongside Vela's own, with doubles dropped by name. What OSM adds is what nothing else has; the switch under Settings > Places turns it off.
  • โœ… Clearer route bar (2026-09-17) - the badges for cameras and crossings now sit beside the bar instead of on it, so the red and amber traffic colors underneath stay visible; fixed speed cameras get a badge too (they were only on the map before); and the remaining-distance label is gone, since the bottom bar already shows it and a long trip's "768.8 mi" ran past the screen edge. Plate cameras on the bar already follow the aiming rule, so one pointed across your road is not marked.
  • โœ… Cross-street callouts place better (2026-09-17) - a callout now steps farther from your road, on whichever side has room, instead of clipping the road you are driving, and a batch that found no labels (its tiles still loading) retries instead of waiting for the next 400 m of driving.
  • โœ… Route time bubbles in the classic chooser, and a way back to it (2026-09-17) - both route choosers now label every route on the map with its time (the bubbles keep apart so parallel alternates stay readable); the Google-style chooser gets a "Compare routes" button that opens the classic list with each route's length, main roads and camera count, and Back returns to it. Opening that list no longer lands the map zoomed in on the destination.
  • โœ… Offline routing offer for your area (2026-09-17) - once, after setup, Vela offers the routing download for the region around Home (or around you with no Home saved), with its real size, so reroutes can use the on-device engine and directions work with no signal. Region sizes in Settings > Offline maps now count the places and map files the download brings along, and a region download takes only its own places and map files (a Northern California download also pulled the whole-state places file, a test bake and Nevada's files).
  • OpenStreetMap shops too (2026-09-17, switch removed 2026-09-23: the places bake carries OSM's businesses and landmarks, so the basemap copy hides over every rebaked archive)
  • โœ… Settings grouped by topic (2026-09-17) - Places (was Place pages) now holds where places come from, what the map draws for them and the place-page toggles; Map is only how the map looks; every camera row and the live traffic re-check sit under Navigation; parking history moved to Saved places; the demo drive, simulated location and the route chooser experiment moved to Diagnostics; "Data source & privacy" is now Privacy. The settings search follows every move.
  • โœ… The mic shows what it understands (discussion #365, 2026-09-14) - the listening dialog lists the command shapes (home and work, go to an address, A to B, nearest X, what is my ETA) so nobody has to guess them; anything else still runs as a plain search. Settings > Search carries the full list too (2026-09-17), grouped by command, in the app language, and every phrase on it is checked against the parser by a unit test
  • โœ… A tapped town label stays in its state (issue #429, 2026-09-14) - tapping Salem, Arkansas on the map used to open Salem, Massachusetts, because the name lookup took the most prominent hit anywhere. A tapped label now only resolves to a listing near the tap, and keeps the label itself otherwise
  • โœ… The date picker shows all seven days on narrow phones (issue #432, 2026-09-14) - the dialog was narrower than the calendar on a 360 dp screen, so the Sunday column was cut off
  • โœ… Transit times are right outside the western US (issue #433, 2026-09-14) - Depart at and Arrive by sent the time as UTC where Google wants the local clock, so anyone east of Greenwich saw schedules shifted by their zone offset (an hour in Britain, three in UTC+3). Fixed
  • โœ… Settings search lands on the row (issue #426, 2026-09-14) - tapping a search result opens the section scrolled to the matching row, which glows for a moment. Parking history is always listed under Navigation, with an empty note until you have parked, so the search never leads to a blank
  • โœ… A new Google session on a schedule (2026-09-23) - Settings > Privacy > "Google session": Vela starts a new Google session every week (default), every day or every time it opens, and "Start a new session now" does it on demand. Google's session cookie is a pseudonymous history of what was looked up under it; rotating caps it. A new session gets a shorter view from Google for a while, hence the weekly default.
  • โœ… Clear history in one place (issue #425, 2026-09-14) - Settings, Data and privacy, "Clear history" wipes recent searches, recent places, parking history and every recorded trip after a confirm. Saved places and lists stay. The per-screen controls (Clear recents on the search page, Clear all under Parking history, per-trip delete under Diagnostics) remain
  • โœ… Zoom buttons on touch phones (issue #393, 2026-09-14) - Settings, Navigation, "Prefer buttons over swipes" now also puts a zoom in / zoom out pill in the bottom-right corner above the parking button, for anyone who would rather tap than pinch. Off by default; always on for phones without a touch screen
  • โœ… Route fit works on tiny screens - the pre-nav framing pads by a share of the visible map rather than a fixed pixel margin, so a 240x320 phone still sees the whole route between the endpoints card and the chooser (#400). The chooser itself opens only as far as the endpoints card leaves over a strip of map, and map icons scale down with the screen density, so low-density phones and car screens no longer draw pins a fifth of the screen wide
  • โœ… Pin stays visible above the sheet - opening a place pushes the map's optical center up by the sheet height (MapLibre bottom padding) and zooms in, so the pin sits in the visible strip above the card instead of being hidden behind it (Google-style)
  • โœ… Popular / busy times (Google-style histogram in the place sheet, day chips + "busy right now") - keyless, 2026-06-19. Two catches fooled us. First: the keyless OkHttp search is bot-degraded (TLS-fingerprint, like photos/transit) and strips the [84] histogram, so we wrongly called it login-gated - a real browser engine isn't degraded. Second (the subtler one): even in the WebView, a bare-name search returns a 20-result [64] list also trimmed of [84]. The fix is a specific query (name + address) - that resolves to a single focused result whose [0][1][0][14] node keeps [84]. WebPopularTimesFetcher (warms google.comโ†’maps, builds the name+address query into the pb + q=, same-origin-fetches it) + PopularTimesParser, wired lazily on every place-open path (search-result tap, recents/saved, POI tap - the latter two were missing it, fixed 2026-06-20). Since the fetch is slow (~10โ€“20 s, real WebView) - and pre-warmed once a search has shown its results (since 2026-09-14 after, not before, the fetch: warming two Chromium instances during a cold-start search held the results at 13 s against 4 s warm), so the first place you open after searching loads faster, the sheet shows a "Loading popular times & detailsโ€ฆ" indicator while it's in flight, so it reads as loading, not missing (clears to the chart, or to nothing for a place with no histogram)
  • โฌœ "hours updated N ago" (place-RPC-only, absent from the search response); Updates/posts tab
  • โœ… Reviews now load MANY (~40, capped 50), not 3 (2026-07-01) - the buzzkill fix. Three root causes in WebReviewsFetcher: (1) the hidden WebView was 0ร—0 (never attached), so Google's virtualized review list - which lazy-loads off the scroll viewport - rendered only its chrome (rating histogram, topic filters) and never the review cards; giving the WebView a real offscreen viewport (measure+layout to 1200ร—3200) makes the scroll pane real so the list renders + pages. (2) The card detector was a fragile "div with one star + text" heuristic that matched the place header ("4.6 stars (57,969)") and affiliate ticket cards instead of reviews; it now selects .jJc9Ad cards directly and accumulates across scroll windows de-duped by data-review-id (the panel recycles DOM nodes on scroll, so any one snapshot holds ~10 - the union is the full list). (3) The "still 3 on ordinary businesses" bug (attractions worked, food/retail didn't): on busy business pages (Taco Bell, restaurants) the Reviews list takes ~8 s to render after the tab is clicked, and the scraper's idle-termination (atBottom && noGrow) was counting that blank pre-render window as "done" - so it bailed with only the 3 overview cards before the full list ever appeared. Fixed by (a) opening via the "Reviews" role=tab (clicked-until-aria-selected, so a click on a not-yet-hydrated tab retries instead of no-opping - and never re-clicks a selected-but-loading list, which restarts its render), and (b) gating the idle-bail on review cards being rendered at bail time (cardsNow, per-tick). (First shipped as a once-latched sawCards flag - that still had a timing hole: the OVERVIEW's 3 preview cards latch it just before the tab click blanks the panel, so unlucky timing still bailed with 3 (seen on Cheesecake Factory). The per-tick check closes it: an empty panel can never satisfy the bail, no matter what rendered earlier. A no-tab/no-button layout - a tiny place whose full list IS the overview - gets a 14-tick leash then returns what's rendered.) Per-review photos are preserved (same per-card scrape). Device-verified: a fast-food branch (612 reviews) 3 โ†’ 50, a busy market food counter 3 โ†’ 37, a landmark 37 โ†’ 46 - all with photos, author, stars, dates. (Ships in the app - compiled JS, not remotely hot-fixable.)
  • โœ… In-app neural voice - runs INSIDE Vela, no standalone app. CURRENT STATE (as of 2026-07-04): the neural voice is Piper. A fresh install downloads HFC Female (en_US-hfc_female-medium, ~67 MB, single-speaker) into filesDir/piper/<id>/, run in-process by PiperSynth (sherpa-onnx VITS โ†’ AudioTrack) behind :core's VoiceGuide/NeuralSynth seam, and it becomes the default once present. Kokoro AND Matcha were tried and REMOVED (Kokoro was ~0.4ร— realtime even on a Pixel 9); MapViewModel reclaims their old model dirs + sanitizes stale vela.kokoro/vela.matcha prefs to Piper. The dated chronology that follows is the HISTORY of how it got here (Kokoro โ†’ +Piper โ†’ Piper-only โ†’ HFC Female default) - read it as a changelog, not the current design; the 126 MB / af_heart / KokoroSynth / filesDir/kokoro references below are all superseded. โคต (Historical:) the first iterations shipped Kokoro (int8 then fp32) and later Matcha as downloadable neural voices alongside Piper, with an installer, playground and upgrade path each. On-device A/B killed both: Kokoro ran ~0.4x realtime even on a Pixel 9 and Matcha read drier than Piper. SIMPLIFIED to Piper-only (2026-07-02): Kokoro and Matcha were removed. Piper is now the sole neural voice ("Vela voice"), default, real-time even on a 5-year-old phone - the right fit for budget Android. VelaKokoro/KokoroSynth/VelaMatcha/MatchaSynth deleted; Settings shows one "Download the Vela voice" button + the playground; system TTS engines still enumerable for override. Stale vela.kokoro/vela.matcha engine prefs sanitize to Piper on launch. (Future "faster Kokoro" would need a different runtime like ExecuTorch - a rewrite, parked.) Voice = libritts_r + speaker picker (2026-07-02): swapped the Vela voice model from hfc_female to vits-piper-en_US-libritts_r-medium (the most expressive Piper English voice - trained on richer speech), which is multi-speaker (904 voices). Since no single speaker is "best", Settings โ†’ Voice โ†’ playground gains a "Voice variant โ—€ N โ–ถ of 904" stepper: each tap speaks a sample and persists the choice (voice_speaker pref; PiperSynth reads + clamps it, exposes numSpeakers). Device-verified: downloads (~82 MB), loads (904 speakers, no crash), stepping speaks a preview in ~410 ms (realtime). Type-a-number jump (2026-07-02): since stepping through 904 one at a time is tedious, the picker also has a "Variant #" number field + Go - type a speaker number and it jumps straight there (clamped to range), persists, and speaks a sample (MapViewModel.setSpeaker). Device-verified: typing 14 โ†’ "Voice variant #14 of 904" + preview. Voice speed + remote-settable defaults (2026-07-02): Settings โ†’ Voice gains a "Voice speed ยท N.NNx" โˆ’/+ 0.1 stepper (0.5โ€“2.0ร—, each tap speaks a preview) - for the neural voice a per-utterance Piper speed multiplier (PiperSynth.speed()), for AOSP engines a live TextToSpeech.setSpeechRate (VoiceGuide.setRate); device-verified 1.00โ†’1.10ร— re-spoke the sample faster. The default speaker AND default speed are remotely settable without an app release: Calibration.defaultVoiceSpeaker/defaultVoiceSpeed ride the signed calibration.json, and PiperSynth/Settings seed the picker + speed from them (a user's own pick always wins). Shipped defaults (calibration v8): speaker 14 (picked by ear) and speed 0.8ร— (a measured, easy-to-follow nav cadence), matched in the compiled Calibration.DEFAULT. Clearer pauses at periods (2026-07-02): the neural voice splits each utterance on sentence boundaries (. ! ?) and splices ~0.32 s of silence between, so a two-part prompt ("Starting navigation. โ€นbeatโ€บ Turn right onto Main Street") gets a real beat instead of running together. The split is name-aware (SpeechText.splitSentences, unit-tested in :core): it only breaks at a period when the preceding word isn't an abbreviation/road word ("Jr.", "Mt.", "St.", "Blvd.", "N.") and the next clause starts capitalized - so "Martin Luther King Jr. Boulevard" is never split mid-name (a bug an adversarial review caught before ship). sherpa-onnx's own silenceScale config was tried first but is a no-op on the Piper/VITS path (A/B-measured on-device: 0.2 vs 1.4 โ†’ identical 2.5 s audio), so the pause is spliced in-app; single-sentence prompts are returned unchanged (no latency cost). Device-verified: a 2-sentence phrase 2.5 s โ†’ 2.7 s with the beat. In-app voice browser - download & switch between many Piper voices (2026-07-03): Settings โ†’ Voice โ†’ Voice library is a browsable catalog of ~23 curated Piper voices (the ones on the sherpa tts-models release), grouped US / British, each row showing accent ยท gender ยท quality ยท size + a one-line "sounds like" note, with per-row Download / Use / ๐Ÿ—‘. Download several, switch between them (Use plays a sample), delete to reclaim space - the โ˜… voices (Lessac, HFC Female, HFC Male, Ryan, the LibriTTS-R default) are the closest to a Google-Maps read. Each voice lives in its own filesDir/piper/<id>/ dir; the installed set is derived from the filesystem (a partial download self-heals), the pick persists in voice_model, and speaker choice is now per-voice (voice_speaker_<id> - libritts's 904 variants don't bleed onto singlepeaker voices; the variant picker hides for those). VelaPiper generalized from one hardcoded model to a selected-id + registry; PiperSynth.reloadVoice() is a race-free single switch trigger (generation-bump + worker-serialized teardown โ†’ no use-after-free of the sherpa engine mid-utterance); a one-time migration relocates the old flat single-voice install in place. Also fixed a pre-existing download bug:** the shared OkHttp client 12 s callTimeout (added for scrape-bounding) was aborting any model download that couldn't finish the body in 12 s - the installer now uses a no-call-timeout client. Device-verified on a Pixel 5a end-to-end: migration preserved the existing voice + speaker, downloaded HFC Female (67 MB), switched both directions (engine reloaded, no crash), two voices coexisting at 149 MB. Catalog is compiled-in for now (PiperCatalog, :core, unit-tested); hosting it on the signed calibration.json is a future bet. Streenumbers read the human way + no clipped start (2026-07-03): three-digit street ordinals are w spoken as people say them - "120th** Street" โ†’ "one twentieth Street", not t neural G2P's stuttery "one, hundred and 28th" (SpeechText.spokenNumbers, unit-tested in :core, wired into VoiceGuide.forSpeech; only 3-digit ordinals 100โ€“999 - espeak reads "5th"/"42nd" fine and 4-digit+ are left alone). And the very first prompt no longer cuts itself off: NavSession.start already speaks the depart ("Starting navigation. Head east on F St"), but NavEngine was *re-announcing the DEPART maneuver** (it sits at the start point, distance โ‰ˆ 0) and interrupting the opener with "a similar directin" - the engine now skips the DEPART entirely and advances silently** to the first real rn (which announces itself on approach, as Google does). Piper's auble in-breaths between clauses are a VITS model artifact, not tunable without a different model - a known limitation.)**Nedefault voice HFC Female @ 0.8ร— (2026-07-03, calibration v10).** After auditioning the browr, the user picked HFC Female**en_US-hfc_female-medium, bright + clear + very Google-like, single-speaker, 67 MB) as the fleet default over libritts. The default voicid is now itself remote-settable - Calibration.defaultVoiceId rides the signed bundle (like defaultVoiceSpeaker/defaultVoiceSpeed), so onboarding downloads it and a fresh install activates it; a user's own voice_model pick still wins. Compiled VelaPiper.DEFAULT_VOICE_ID + calibration.json v10 both set HFC Female + speed *0.8ร—** defaultVoiceSpeaker=14 stays but now only tunes libritts (the multi-speaker voice) when someone picks it. Nav pmpts already carry the commas/periods that make Piper flow (the "In , " comma + the sentence-split pause), so no manual punctuation is needed on real routes. Voice library goes multilingual (26-07-03, first step of app localization): PiperCatalog generalized from US/GB-only to any language - it derives langCode/klzzh:1238 from the voice id and the browser now *groups b language (English first, then by endonym: Franis, Deutsch, Espaรฑol, Italiano, Portuguรชs, Nederlands, ะ ัƒััะบะธะน, Polski, Svenska, ะฃะบั€ะฐั—ะฝััŒะบะฐ), each with a recommended default (fr_FR-siwis, de_DE-thorsten, es_ES-davefx, it_IT-paola, pt_BR-faber, nl_NL-alex, ru_RU-irina, โ€ฆ). You can download + audition any of them in the playground now; full locale pairng(French UI + French nav xt + a French voice, auto-suggested from your phone's language) lands with the localization work in progress. Spoken navigation now speaks 10 language(2026-07-03). The whole genated nav voice - turns, distances, the "In X, โ€ฆ" frame, lane guidance ("use the right 2 lanes"), start/arrival/stop/faster-route - is localized via a per-language NavStrings table in :core, and auto-follows the phone's language** klzzwxh:1248, default system):nglish, French, German, Spanish, Italian, Portugue, Dutch, Russian, Polish, Swedish, Ukrainian**. These are per-language templates, not word swaps, so grammar is right (German dative "In 400 Metern", Slavic plural-noun agreement, Italian side-noun vs adverbial, Dutch separable verbs "de X op", Swedish "engelsk mil" to disambiguate the 10 km Scandinavian mile) - each translation drafted then corrected by a native-speaker review pass. English stays byte-identical (existing nav tests untouched); road/street names are never translated (they're data). Unit-tested (every language non-blank + preserves road names). A French phone already gets fully French turn-by-turn given a French voice; the visible UI chrome + Google POI content localize next. Voice โ†” language pairing (2026-07-03). So the nav text and the voice speak the same language, onboarding's one-tap install now grabs the voice that matches the app language (PiperCatalog.defaultFor(lang) - a French phone downloads fr_FR-siwis, not HFC), the Voice library floats your language's voices to the top (Google-style), and if your language is non-English with no matching voice installed it shows a download nudge ("Your app language is Franรงais - download a Franรงais voice so spoken directions match"). English still gets the remote-settable fleet default (HFC). In-app language picker (2026-07-03). Settings โ†’ Language lets you override the app language independent of the phone's system setting (Google-Maps style): "Follow system" (default) or any of the 11 by endonym (English, Franรงais, Deutsch, Espaรฑol, Italiano, Portuguรชs, Nederlands, ะ ัƒััะบะธะน, Polski, Svenska, ะฃะบั€ะฐั—ะฝััŒะบะฐ). It drives AppLocale โ†’ the spoken-nav language today (and the voice-library sort/nudge); the visible UI chrome switches with it once the strings.xml extraction lands. UI chrome externalized to resources (2026-07-03). All ~330 user-facing :app strings (Settings, place sheet, map, search, nav overlays, welcome, the foreground-nav notification) were moved out of inline Kotlin literals into res/values/strings.xml (English), referenced via stringResource/getString - the foundation for translating them. English is byte-identical (no visible change); a runtime locale switch (MainActivity.attachBaseContext + AppLocale.wrap, no-op when following system) re-reads them in the chosen language after a recreate(). A handful of dual-purpose literals were deliberately left inline (they double as logic keys - "Open"/"Closed" feed the status-color parser, the search-along-route chips "Gas"/"Food"/โ€ฆ are also the query, the review sort/tab labels branch a when); those localize once display text is decoupled from the logic key (same "decouple detection from English" linchpin as the POI content). The visible UI now speaks 10 languages (2026-07-03). The externalized strings were translated (machine-translated then native-speaker-reviewed, per language) into French, German, Spanish, Italian, Portuguese, Dutch, Russian, Polish, Swedish, Ukrainian - res/values-<lang>/strings.xml ร— 10 (330 strings each). Every translation's format-placeholder set was validated against English before writing (a mismatch omits that one string โ†’ Android falls back to the English default, never a crash); all 3300 passed. Picking a language in Settings โ†’ Language (or following the phone locale) re-creates the Activity and the whole app - Settings, search bar, place sheet, nav overlays, welcome, the foreground-nav notification - renders in that language, alongside the already-localized spoken directions. Device-verified on a Pixel: English โ†’ Franรงais switched the entire UI live (Paramรจtres, Impรฉriales, Langueโ€ฆ), persisted across navigation, and round-tripped back to English with no crash; place names stayed untranslated (data). The native reviews caught real localization bugs the raw MT missed (French "Mo" not "MB", Italian "Passaggi" for route steps so it doesn't collide with "tappa"top, German noun capitalization, French % spacing). **Google POI content now mes back in your language too (2026-07-03) - the last i18n layer. The scrape's hl=en is rewritten to the app/system language at reqst time (GoogleMapsDataSource.localized(), no-op for English โ†’ English users byte-identical), so categories, hours, open/closed status, price and distances arrive localed* ("Ouvert ยท Ferme ร  19:00", "Pizzeria", "09:00โ€“19:00", "10โˆ’20 $", "549 m"). The linchpin - open/closed detection used to string-match English "Open"/"Closed" and would have broken - is solved by **matching the localized status TEXT against a per-language keyword table** (SearchParser.parseOpenNow(status, lang), closed-words-first for every language, lang = the same Locale.getDefault() that set hl=); placeStatusColor colors from that boolean, so an open plae is green in every language. Correction 2026-07-04: the rst fix read a "locale-independent numeric status code" pinned from an hl=fr capture - a live EN capture then DISPROVED those ints (closed pharmacies carried the "open" code 6, an Open-24-hours business carried 13/4 โ†’ rendered red); they're span/style markers, not status codes, and the code path was removed. Text is authoritative - it's the same string the user sees, so color can never contradict the words.)Permanently-cled uses the numeric [23] flag, which IS real. Device-verified on a Pixel:** searched in Frencโ†’ results + place sheet showed French categories/hours and a green "Ouvert"* English unchanged (still "Open ยท Closes 7 PM" green). Pinned by driving a temp hl=fr build + reassembling the 771 KB response from logcat. *Voice โ†” anguage mismatch handled (2026-07-04). A Piper voice is a **single-language** model, so overring the app/system language to one whose voice isn't downloaded made the nstallvoice read the new language - the English HFC voice literally reading ssian nav text (gibberish). Now NeuralSynth.voiceLanguage exposes the loaded voice's language (id prefix) and VoiceGuide.speakNow compares it to the language the nav text is generated in (NavStringsRegistry): on a mismatch it routes to Android klzzwxh:127 in the target language itead (lazily creating a default engine as the fallback - the system TTS is no longer shut down when the neural voice is active), and if the system has no voice for that language either it stays silent** rather than mangling it, flashing a **"get <language> voice in Settings โ†’ Voice"** hint (langUnavailable โ†’ MapViewModel). Same guard covers a whole-phone locale change. Device-verified on a Pixel 5a: app language โ†’ ะ ัƒััะบะธะน with only the English voice installed produced no neural playback (correctly bypassed, no crash); switching back to English spoke normally through the neural voice (PiperSynth: spoke 3.9s audio). Voice library language groups collapse (2026-07-07). The ~40-voice catalog was one long flat scroll (a Reddit report). Each language is now its own collapsible sub-group with a voice-count badge and a chevron; a group opens by default only when it's your app language or already has a voice installed, so you see your own language plus whatever you have and the rest stays folded. A search in the box forces every group open so matches aren't hidden. Per-language toggle state (langExpanded map in VoiceLibrary); the whole Voice library section stays behind its own collapsible header as before.
  • โœ… POI hero photos - LRU cache (2026-07-02): WebPhotoFetcher keeps an access-order featureId โ†’ List<Photo> cache (cap 32), so re-tapping a place (or bouncing back from directions) shows its gallery instantly instead of re-running the ~20 s scrape. (They already used the warm, streaming hidden WebView + the instant search-response preview; this closes the revisit gap.)
  • โœ… Reviews pivot follow-ups (2026-07-02). (a) Native review search emboldens the matched term(s) in each result (author + text, case-insensitive emphasize() โ†’ AnnotatedString bold). (b) Full-screen panel bugs fixed: it rendered nothing until you toggled a chip, and photos/videos wouldn't open - both were the nested-panel carve fighting Google's own page. In full-screen the carve no longer pins [role=main] (position:fixed made main the containing block for Google's position:fixed lightbox โ†’ off-screen; overflow-y:auto hijacked scroll from Google's inner list so its virtualizer never paged โ†’ the "select a chip then back to All" bug) and skips stretch(); content now renders on first paint (device-verified 128 cards immediately) and review photos are tappable โ†’ Vela's native gallery (Google's own photo viewer is a page-nav the lockdown blocks and the carve can't host - verified a raw click with the carve fully removed still opens no viewer, so we intercept โ†’ native PhotoGallery, captioned "Author ยท date"). (Video reviews still don't PLAY in full-screen - same nav/host limitation; a video's poster may open as a still. Open.) (c) Place sheet handle: bigger hit target (36dp tall strip), tap toggles expand/peek, and scrolling into the content grows the sheet to full height Google-style (the nested-scroll handler expands on an upward scroll). Device-verified: bolded "beer" matches, full-screen photoโ†’gallery, smooth Google scroll. (d) Native review photo caption reliability (2026-07-02). The inline review-photo caption ("Author ยท date") is only as good as the DOM scrape, and two things made the date and the photo strip flaky place-to-place (why author/date showed on one device/place but not another - same build, different live-DOM luck, NOT version skew): the date fallback grabbed the first span merely containing "ago" (so a short sentence in the review body could win) and only knew English "ago"/bare-year; and the photo collector read only <button> background-images, so a card whose uploaded photos render as <img> tiles got no tappable strip at all. Hardened in WebReviewsFetcher: the date fallback now anchors to the full relative-date shape ("10 months ago", "a year ago", "Edited 2 weeks ago") or a lone year, skips owner "Response" lines, and skips any span whose text is part of the review body; the photo collector now also sweeps <a> backgrounds and <img> src (avatar-filtered) so <img>-rendered photos are caught. (Hero photos above the place name are a separate, still-unbuilt case: the fast WebView gallery scrape captures only image URLs - no contributor name or date - so place.photoDates is empty on that path and the hero caption has nothing to show. Adding it is net-new gallery-DOM scraping, tracked separately.) (e) Caption invisible on Android 15/16 - the real root cause (2026-07-04). The "author/date still doesn't show" report reproduced on a stock Pixel 9 (Android 16) but NOT a Pixel 5a (Android 14). It was NOT the scrape (an on-device dump proved the caption reached PhotoGallery correctly, val=[Theresa Jerome ยท 7 months ago]) and NOT off-screen (a position probe showed it laid out on-screen at yโ‰ˆ2244 of 2424). The real cause: a full-screen Compose Dialog on Android 15/16 doesn't propagate window insets to its own content (navigationBarsPadding/safeDrawingPadding read ZERO inside it) AND its window is clipped by the nav-bar strip - so a BottomCenter caption was measured on-screen but sat in the clipped bottom ~180 px and never painted (a bright-red debug background at the bottom drew nothing; the same caption at Center drew fine - that isolated it). Reading the parent's insets didn't work either (the place-sheet is itself a modal, so its inset context is wrong). Fixed with a fixed bottom clearance (padding(bottom = 88.dp)) that keeps the caption in the drawable area regardless of the broken insets, plus a dark scrim pill for legibility. Device-verified on the Pixel 9: "Theresa Jerome ยท 7 months ago" now shows. (The earlier safeDrawingPadding attempt in 0.2.285 was based on a wrong overflow theory and didn't fix it.)
  • โœ… Reviews = native inline list + full-screen "Read all" Google panel (2026-07-02, THE ARCHITECTURE PIVOT - supersedes the inline-embedded panel below). After days fighting scroll jitter in the inline-embedded panel, a workflow probe confirmed it's inherent: a Chromium-composited scroller nested inside a Compose scroll can never share a frame clock, so the top-boundary seam is always latent (three unsynchronized loops meet there - stale JS edge-state gating at touch rate, dispatchRawDelta racing the WebView's own scroll, async header re-insertion on disengage). Resolved by SPLITTING: inline place-sheet reviews are now Vela's own NATIVE list (ReviewsTab, a native LazyColumn) - smooth by construction, zero jitter, zero Google-chrome flash; review photos are tappable โ†’ the shared PhotoGallery, captioned "Author ยท date" (device-verified "Sammie Smith ยท 10 months ago"). A "Read all reviews" button opens the live Google panel FULL-SCREEN (FullScreenReviews โ†’ GoogleReviewsPanel(fullScreen=true)): no nesting, so Google's own infinite scroll, server-side search, Sort, topic chips, AND native photo/VIDEO viewers work (videos play) - dark-themed, back-to-close top bar. In full-screen the carve is lighter (a FULL flag skips the scroll-sync touch listener, the photo interceptor, and the native histogram/chips carve - Google's own controls show). The LiveReviews toggle now gates that button (Settings โ†’ Map: "Read all reviews button"). The native scrape (WebReviewsFetcher, ~50-cap, streams in) always runs now. Device-verified end-to-end (Boundary Bay: native list streams smoothly, photoโ†’gallery caption, Read-allโ†’full Google page with histogram/search/chips/photo-grids, back closes clean). (The old inline scroll-sync machinery - delta-forwarding, engaged mode, edge bridge - is retired from the inline path; that code now serves only the full-screen view, where nesting doesn't exist so it's mostly dormant.)
  • โœ… LIVE Google reviews panel - inline-embedded (2026-07-01) โ†’ now the FULL-SCREEN "Read all" view (see pivot above). (History of the embedded approach retained below.) The Reviews tab embedded Google's own reviews pane in a visible WebView, CSS-carved down to just the panel: loads at browser speed, auto-pages as you scroll (no polling loop, no cap - all 612 of a Taco Bell's reviews are reachable), and keeps Google's own review search + Sort (server-side, searches ALL reviews - the native search only filtered the ~50 scraped). Dark/light follows Vela's theme (invert+hue-rotate filter scoped to the panel, images re-inverted). Trackers/beacons are blocked at the network layer (shouldInterceptRequest feeds /gen_204, play.google.com/log, analytics/doubleclick/adservice hosts an empty response) and every navigation after the initial load is blocked - no tap can leave the panel. When the toggle is off (or the carve fails - Google markup shift), the tab falls back to the native scraped list automatically, and the background scrape only runs in that fallback (no wasted 20 s scrape behind the panel). The panel is stripped to just the reviews - Google's Overview/Menu/Reviews/About tab bar, the "Order online" promo block, the "Write a review" button, and each review's Like / Share / โ‹ฎ buttons are removed (the last is a deliberate block - it leads to Google sign-in), and the background is Vela's exact sheet color (SheetPalette #1F1F1F dark / #FFFFFF light) so there's no seam: <body> carries the color while the panel content and every ancestor are transparent (in dark, only the content inverts, not the backdrop). Google's own controls work embedded (2026-07-02): Sort (Most relevant / Newest / Highest / Lowest) and the per-review/photo popups were dead because Google renders them into portal containers the carve had hidden (empty at carve time); a revealOverlays() pass un-hides those portals on tap, lifts them above the panel, clamps a menu that would overflow the viewport, and (dark) inverts them to match. The reviews-search keyboard now drops on submit (blur on Enter/search). Dark-mode rating stars are re-inverted to true gold (a scoped double-invert). Tapping a review photo opens Vela's own full-screen gallery (2026-07-02) - Google's embedded viewer renders nothing inside the carve, so the tap is intercepted, that review's photo URLs are collected + upsized (=s1600) and handed to the native PhotoGallery, captioned "Author ยท date" (e.g. "A reviewer ยท 5 months ago" - author from .d4r55 / the avatar's "Photo of โ€ฆ" label, relative date from .rsqaWe). Photos are keyed off jsaction*="review.openPhoto", NOT the aria-label (fixed 2026-07-02): Google labels some photos descriptively ("Mixed dumplings with rye breadโ€ฆ") instead of "Photo N on โ€ฆ", so the old label-regex silently dropped them - the tapped one wouldn't open and the strip collected only its siblings (the "first pic won't open, shows 1/1" bug, reproduced + DOM-verified on a review with a descriptively-labeled photo: 2 photos, only 1 matched โ†’ now 2/2). The URL now reads from inline bg, computed bg, or a child <img>. The panel now shows only a spinner until the reviews actually paint (gated on review cards present, with a grace fallback for zero-review places / class rotation) - no more Order-online-button flash during load. Scroll-sync (2026-07-02, v2 - manual delta-forwarding): the panel used to hog every vertical gesture (its OnTouchListener always requestDisallowInterceptTouchEvent(true)), so a finger on the panel could never move the Vela sheet - once you scrolled into the reviews you couldn't scroll back out (the panel fills the sheet). Now the page reports the reviews scroller's top/bottom edge over a JS bridge (onPanelEdge), and at a boundary - reviews at their top + finger dragging down, or bottom + up - the touch listener forwards the raw drag deltas to the sheet, which scrolls its body 1:1 with the finger (bodyScroll.dispatchRawDelta), then past the body's own ends mirrors the sheet's normal drag behavior (collapse โ†’ dismiss on a pull past the top; expand on a push past the bottom); finger-up velocity carries a boundary fling into the sheet. Mid-list gestures stay 100% with the reviews. v1 tried gesture HANDOFF instead (flipping requestDisallowInterceptTouchEvent(false) at the boundary so Compose would steal the stream) - don't go back to that: Compose's drag detector has already abandoned a stream whose early events the WebView consumed, so the flip did nothing for real fingers/flings (a slow synthetic swipe happened to pass) and the panel stayed a scroll trap. Delta-forwarding transfers no ownership, so it also survives mid-gesture direction reversals. Device-verified end-to-end: from deep in the reviews, repeated down-swipes walk the reviews to their top, then the sheet body back to the photo hero, then collapse the sheet to peek (and on through to dismiss). Polish round (2026-07-02, from real-finger feedback): an inertial fling that lands on the reviews' top edge now hands its leftover momentum to the sheet (the page tracks a low-passed scroll velocity and reports an edge-fling over the bridge; forwarded only when no finger is down) - no more dead-zone snap; when the user starts really scrolling the reviews, a one-shot engagement signal auto-expands the sheet + settles the body so the panel takes the screen Google-style (re-armed when the reviews return to their top), and the panel is sized to fill the expanded sheet; the rating histogram is scraped off the panel's DOM (tr[aria-label] "N stars, M reviews" rows) and drawn natively in Vela's rating header (amber bars - the panel and the scraped-fallback modes now share one look), with Google's in-panel summary block faded out (opacity, NOT display:none, and only after cards exist - removing it from layout while Google's virtualized list mounts corrupts the list's offset math and the SPA permanently unmounts every card; reproduced live, twice); the load spinner anchors at the panel's top (visible as you scroll toward it); while the panel loads the featured review from search shows as a teaser; reviewer-name/review-count links lose their underline/tap-flash (navigation was already blocked); and newly-paged cards get their Like/Share stripped immediately (MutationObserver) instead of at the next 1 s tick. Feed watchdog: Google sometimes serves the page shell but silently withholds the review feed (soft bot-throttle - observed under heavy testing: tabs + histogram render, the feed request is never issued; the rate bucket is per client fingerprint - the hidden scraper pulled 55 reviews off the same page seconds after the panel got zero, and unblocking telemetry changed nothing, so the tracker blocking stays); ready-but-cardless for ~15 s now fails over to the native scraper instead of leaving an empty panel. Round 2 (2026-07-02): the native histogram is narrower (0.62 sheet width) and hides while reading reviews full-screen (returns on the drag back up); the "Reviews are automatically processed" โ“˜ row is carved out with the summary - two-phase: opacity immediately (kills the flash of Google's own histogram; zero layout risk), display:none only after the feed has been healthy ~5 s (reclaims the blank space); and elements set in "Google Sans" are rewritten to the system font (generic sans-serif - inherits the phone's font choice, not pinned to Roboto), icon fonts untouched. Round 3 (same day, real-finger feedback): engaged reviews mode now clears the whole native header - rating row AND the Reviews/About tab bar (nothing floats above the reviews; everything returns when you walk the sheet up), and disengage happens at gesture end (re-inserting header content per-pixel mid-drag shifted layout under the held finger - read as flicker); the summary fade runs in the pre-ready loop + on DOM mutations (recycled nodes re-fade within a frame - the "saw the google histogram for a second" flash and top-area flicker); the collapse only fires while the list is at its top (collapsing 170px of content above a mid-list reader yanked the view - the "flung way down"); the engagement threshold went 40โ†’120px of real scrolling (casual drags no longer trigger the takeover); underline suppression now covers buttons (author name/photo count are <button>s, not links); dark-mode stars get saturate(1.7) brightness(1.12) after the re-invert (a 0.92-invert + 1.0-re-invert compresses color toward mid - mathematically can't restore full gold, so it's compensated). Round 7: the native histogram is centered; businesses with no auto-parsed topics still get the native search + sort (the empty chips case was holding the whole control row hostage - the page now reports "no topics" once the feed is healthy and the controls render chip-less); full gesture smoke passed on-device (engage โ†’ deep scroll โ†’ walk home with header restore โ†’ collapse โ†’ dismiss, no flicker, no stuck states). Native search / sort / topic chips (2026-07-02, task 81): Google's in-panel search box, Sort button, and auto-parsed topic chips ("potato dumplings" ร—50) felt out of place - they're now Vela-native: the chips are scraped off the page over the bridge (anchored by the "All" chip - position, not classes), rendered as Vela FilterChips beside a native search field (IME-search submits) and a native Sort menu (Google's four orders), all driving Google's hidden originals (window.velaSearch/velaClickChip/velaSort - fill-and-Enter the input; click the matching chip/menu item). So the search stays Google's server-side one across ALL reviews, wearing Vela's UI; Google's own rows are carved out (same two-phase fade/collapse policy) only once the chips data reached Vela. Photos stream + warm (task 82): the gallery scrape now reports partials whenever its accumulated set grows - the strip fills progressively (first partial = the page's hero photos, ~1 s after load) instead of arriving all at once ~20 s later (monotonic + feature-id-gated like review streaming) - and the first search pre-warms the photo WebView (renderer + HTTP/2 sockets + cache) so the first place open skips the cold start. Round 6 (feel fixes): engaged mode could never disengage - its exit test ("body walked up 150px") was unreachable once the panel filled the sheet (the body's whole range is smaller than that), so the header never returned and top-drags collapsed/dismissed instead; disengage now also fires at body-top and on any pull-collapse/dismiss. The panel is one constant full-sheet size - engaging only hides the header (a WebView RESIZE paints a frame late in Chromium theeported black-band flicker). And the reveal-hold now waits for all** carves (summary + Google's search/cps rows, which fade once the chips scrape lands - now also in the pre-paint microtask path), not just the summary. Round 5 the watchdog is class-agnostic: Google A/B-serves front-end buds with rotated class names; on those, cards render fine but the class-based check counted zero and the watchdog killed a HEALTHY panel mid-read (user report). velaHasReviews() keys on relative-date texts ("2 months ago") - content can't rotate; a genuinely empty feed still fails over. *Round 4: the ummary fade also runs from a document-level MutatiObserver - callbacks fire at *microtask* timing, before the browser pats, so Google's histogram can ever* flash screen (the 250ms-tick fade left a visible-frame window; disconnects once landed); and engaged mode now sizes the panel to fill the enti sheet (hiding header pies one at a time was whack-a-mole - the popular-times graph slid into the vacated window and floated; past the takeover point it's purely reviews, everything redraws on the walk back up). Four adversarial-review-caught gesture bugs fixed befe ship:** (1) deltas are measured in window space (view-local dy shrinks by however much the forwarded scroll just moved the WebView - the sheet tracked at HALF finger speed with every-other-frame stepping, and the fling velocity halved); (2) multi-pointer: an active-pointer id is tracked and re-based on ACTION_POINTER_UP (before, lifting one of two fingers read the other finger as one giant delta - enough to instantly collapse/dismiss the sheet); (3) leaving the boundary mid-gesture ends the forwarding (resets the pull accumulators + disarms the end-fling - a drag that touched the boundary then reversed into normal review scrolling no longer flings the sheet at finger-up); (4) a fling needs >24px of forwarded travel (a tap with a jiggle can't launch the sheet). Plus: content paging grows scrollHeight without a scroll event, so a MutationObserver re-reports the edge state (stale at-bottom briefly double-scrolled up-drags). Maintenance is now lifetime, not 60 ticks (same fix): the carve's upkeep loop (strip/stretch/revealOverlays) stopped after ~60 s, so review cards paged in later kept their Like/Share buttons AND a swapped scroller lost its stretch + edge hooks; it now runs every 1 s for the WebView's lifetime (the WebView dies with the sheet, killing the loop). (Not doable: a Like-count filter - Google login-gates the "helpful" counts, so every logged-out review reads 0. The review video player remains open.) Built by interrogating the live page over Chrome DevTools protocol; the carve recipe's non-obvious load-bearing parts are documented in ReviewsPanel.kt (vh units are 0 in the embedded WebView - everything is sized in px; the ancestor chain must be un-clipped AND un-transformed or nothing paints; the scroller is stretched only after the Reviews tab reports selected; disallow-intercept must be re-asserted on EVERY touch event or the sheet steals the gesture). - the Reviews tab shows a "Search reviews" box (appears once โ‰ฅ5 reviews loaded) that live-filters the loaded reviews by text or author, case-insensitive; an empty result reads "No reviews mention "โ€ฆ" (searching the N loaded)" so it's clear the filter is over what's on-device, not a fresh server query. Clears per place. Device-verified on Taco Bell (typing "cramped" narrowed 50 โ†’ the one review that mentions it).
  • โœ… Live review-loading progress (2026-07-01) - the scrape legitimately takes ~10โ€“40 s on busy places, so the Reviews tab shows real progress instead of a bare spinner: the scraper streams its running count over the JS bridge (VelaBridge.onProgress, reported whenever accN changes) โ†’ MapState.reviewsFound โ†’ "Loading reviewsโ€ฆ N of ~M" with a determinate progress bar (M = the place's own review count capped at 50, i.e. what the scrape can at most deliver); before the first card lands it says "this can take half a minute" so the wait is expected, not a hang. Feature-id-gated (a slow scrape can't tick a different place's counter). Device-verified on Cheesecake Factory (10 โ†’ 19 of ~50 with the bar advancing). Reviews STREAM IN under the bar (2026-07-01): the scraper also sends the accumulated reviews themselves whenever the count grows (VelaBridge.onPartial โ†’ fetchReviews' streaming callback โ†’ the list renders below the progress header while it's still running) - so you read reviews from the first seconds instead of staring at a bar for 30 s. The partial-vs-final race is closed atomically (the streaming update is gated on reviewsLoading inside the state CAS; the final result clears that flag in the same copy, so a straggler partial can't overwrite the complete list), and a Kotlin-side timeout after partials keeps the streamed set instead of wiping the list (empty < partial < full). Idle patience fixed (the "Taco Bell stopped at ~15" bug): Google's lazy pager routinely takes >2.2 s to fetch the next ~10 reviews on a busy place, and the old 4-quiet-ticks idle-bail misread that as "done" - the opened list now waits 8 no-growth/6 at-bottom ticks (~4.4 s) before concluding it's exhausted; the unopened overview keeps the short fuse. Device-verified: Taco Bell (612 reviews) delivers the full 50 (cap) with reviews visibly streaming in mid-scrape. (Review-pass hardening, same day: a wedged scrape that streamed only a sliver still auto-retries - keeping the streamed set must not disable the retry loop, so it re-runs when the result is < min(4, count); an in-flight scrape is a cancellable job superseded by the next place's fetch and canceled on deselect/pin - before, an abandoned 40 s grind held the fetch mutex and queued the next place's reviews ~90 s worst-case; and the search box waits for completion so it doesn't shift rows mid-read - it takes the space the progress header vacates.) 2.1ร— faster (2026-07-01): the scrape ran at the pace of its own conservative polling, not Google's (an A/B with onResume/resumeTimers ruled out background-timer throttling - 37.4 vs 38.8 s). Three measured cuts: the poll loop ticks every 250 ms (was 550) with every grace/patience window rescaled to keep its wall-clock size (38.8 โ†’ 24.5 s); the offscreen viewport grew 3200 โ†’ 6000 px - a virtualized list renders/pages to FILL its viewport, so taller = more reviews per cycle - and the post-onPageFinished settle dropped 800 โ†’ 150 ms (the script self-polls; together 24.5 โ†’ 18.6 s). Final: Taco Bell 50 reviews in 18.6 s, a landmark in 24.9 s, first reviews streaming at ~3-5 s (page load + inject is only ~3 s - the rest is Google's own render + pager fetches). Deeper cuts need the visible-WebView architecture (see ROADMAP), not tuning. Stale-flag hygiene: a long-press dropped pin never fetches reviews or photos, and the previous place's in-flight fetches complete behind feature-id gates that no longer match - so onMapLongPress now clears reviewsLoading/reviewsFound/photosLoading/loadingDetails like the POI-tap path does (before this, a pin dropped mid-scrape showed photo shimmer tiles on a bare road forever).
  • โœ… Place actions in a Google-style quick-action row (circular icon + label): Call (dialer), Website, Save, Share menu (Google Maps link / Map pin geo: / coordinates / address) - the actions are evenly weighted across the full width so all five fit on one row without the trailing Share icon clipping off the edge. The geo: pin is the degoogled-friendly share (geo:lat,lng?q=lat,lng(Name) opens in any maps app, incl. Vela - no google.com); round-trips back through Vela's own MapLinkParser (unit-tested)
  • โœ… Place photos - full gallery, keyless via place-page scrape (2026-06-28). The photo strip leads the sheet as a hero (horizontally scrollable; tap โ†’ full-screen, swipeable gallery with a counter). It opens instantly with the search-response preview - the hero block at [1][72][0][i][6][0] (Google moved it [105]โ†’[72] on 2026-06-27; hot-fixed via calibration v7), de-duped (Google serves the single hero twice) plus the small landmark-only [1][204][0] block - then the full gallery (~9โ€“25 photos) swaps in a few seconds later. That gallery now comes from WebPhotoFetcher loading the place's own ?cid= Google Maps page in a hidden WebView and scraping the rendered photo URLs out of the DOM (googleusercontent, avatars/Street-View filtered, de-duped by image id), the same trick as the reviews scrape. This replaced the bare hspqX RPC, which Google bot-degrades to a per-session Street-View-only reply (verified on-device: byte-identical degraded replies across retries - the page render is far harder to degrade than a naked RPC POST). While the scrape is in flight a row of pulsing shimmer placeholder tiles shows so it reads as "more photos loading" (PlaceSheet.PhotoShimmerTile driven by MapState.photosLoading). On-device-verified 9/10/23 photos across three places. (No posted-date from a DOM scrape - that was an hspqX-only field.)
  • โœ… Photo category tabs (Menu / Food & drink / Vibe / By owner) (2026-07-01) - the gallery now carries Google-style category filter chips. Google keeps those tabs in the ?cid= page DOM (verified on-device), so WebPhotoFetcher visits each category tab in turn (clicks it, scrolls, tags the photos it shows with that category), sweeps "All" for the rest, and returns categoryโ‡ฅurl lines; Photo.category + index-aligned Place.photoCategories carry it through. The place sheet shows an All + <categories present> chip row above the strip; tapping one filters the strip (and the tapped photo still opens at the right spot in the full-screen viewer). Device-verified on The Cheesecake Factory: All / Menu / Food & drink / Vibe / By owner chips, and "Food & drink" correctly drops the exterior shot to show only food photos. (Keyless - the tabs render to a logged-out browser. Ships in the app.) Populated FULLY, not 1-per-category (fixed 2026-07-01): the category grids are virtualized like the reviews list, so at the headless WebView's 0ร—0 size each tab rendered ~1 tile; giving the photo WebView the same offscreen viewport (1200ร—3200) + collecting across scroll within each tab now yields ~15-30 photos per category (device-verified Cheesecake Factory: Menu 24 / Food & drink 15 / Vibe 17 / By owner 13). Cap raised to 80 so later tabs aren't starved; tab names filtered to clean category words (a "Menu ยท Photo 1 of 12" caption was being mistaken for a category).
  • โœ… Full gallery via a hidden WebView (WebPhotoFetcher) - the gallery RPC (batchexecute hspqX / /MapsPhotoService.ListEntityPhotos) serves the user photos only to a real browser engine. A plain HTTP client - even with perfect headers + consent cookies - gets a degraded Street-View-only reply: bot-detection at the TLS/fingerprint level (verified on-device - OkHttp gets a 162 KB token-less "lite" /maps page). So Vela runs a hidden Android WebView (real Chromium) that loads maps.google.com as an anonymous, no-login session - exactly like a logged-out browser, which does show the photos - and does a same-origin fetch to the RPC, handing the raw response back over a JS bridge. Keyless (no API key, no account). Created lazily (only when a place's photos are wanted), strictly best-effort (failure โ†’ keep the preview). On-device verified 2026-06-17: 31 photos for SpeeDee-Midas, Davis. Tradeoff: the WebView runs Google's JS (a fingerprinting step for a degoogled app - the opt-in cost of richer photos), OkHttp fallback kept. Gotchas baked in: desktop UA (a mobile UA makes Google deep-link to intent:// the native app), block non-http(s) redirects, and Handler, not View.postDelayed (a headless WebView never attaches to a window, so View timers never fire).
  • โœ… Category quick-chips (Restaurants/Coffee/Gas/Groceries/Hotels/Pharmacy/ATMs/Parks) โ†’ one-tap search, each with a Google-style leading icon
  • โœ… "Search this area" - re-search after panning the map. Fixed to use the panned viewport (2026-07-02): the button searched around a stale mapCenter (only refreshed on a POI tap / result select, not on a plain pan), so panning across town then tapping it re-searched where you were. onViewport now writes the live viewport center to mapCenter on every camera-idle, so the search always targets what's on screen.
  • โœ… Filter: open now, rating โ‰ฅ 4.0, and price (tap the Price chip to cycle โ‰ค$ โ†’ โ‰ค$$ โ†’ โ‰ค$$$ โ†’ โ‰ค$$$$ โ†’ off, filtering on priceLevel) - chips sit on their own horizontally-scrollable row in the results header, stackable
  • โœ… Saved / favorite places (star from the place sheet) - reopening a saved place enriches it via search so photos, rating and reviews load (saved places carry no feature id of their own); each saved row in the search page has a โ‹ฎ menu to Set as Home / Set as Work (promote it straight to a shortcut) or Remove it (verified on-device)
  • โœ… Export / import saved places (Settings โ†’ Saved places) - Export writes the starred list to a portable JSON file shared via the system sheet (same FileProvider as the diag export); Import picks a file and merges it (de-duped by id, never overwrites/removes), with a toast of how many were added. Keyless, local, portable between devices.
  • โฌœ Overture/OSM POIs as a fallback source

Routing & traffic

  • โœ… Interchangeable transit lines no longer read as transfers (issue #234, 2026-08-03). A direct connection served by several equivalent lines carried every alternative as its own summary badge, so the results list looked like a multi-change journey while the opened itinerary correctly showed one train. When the summary badges outnumber the actual ride legs, the alternatives now merge into one slash-joined badge per leg (S1 / S11 / S12) in the ridden line's colors; mismatched shapes pass through untouched rather than guess.
  • โœ… Driving directions with real traffic-aware ETA (live duration_in_traffic)
  • โœ… Live traffic overlay (browse mode) - Google's congestion-colored roads, a keyless raster layer (the web map's own public /maps/vt?โ€ฆ!2straffic PNG tiles on www.google.com - no API key). Toggle moved off the map into Settings โ†’ Map (2026-06-19) - it's a niche browse-only layer now that nav shows per-segment route traffic, so it no longer earns a map button. Drawn below the POI/label layers at ~0.6 opacity so it doesn't render over POIs or bury the basemap. (Inherent caveat: Google's pre-baked raster paints free-flow green everywhere and re-rasterizes on zoom - it's a subtle scanning aid, off by default.)
  • โœ… Per-segment route-line traffic - the drawn route is colored Google-style along its length: free-flow blue with amber/red/dark-red bands over the congested stretches, from the directions response's own congestion spans (route[3][5][0] = [level, startMeters, lengthMeters], only the non-free-flow runs - parsed sorted by start so the bands walk the line startโ†’end in order). Rendered as solid color bands (a MapLibre step expression, not an interpolated gradient - the driven/ahead boundary and span edges are crisp, per test-drive feedback "should be solid"). Combined with the driven-gray split so the part behind the vehicle grays out - but only while actually navigating: a pre-nav route preview draws clean with no gray nub at the start. (Walk/bike and no-live-traffic routes fall back to a single overall blueโ†’amberโ†’red tint.)
  • โœ… Alternative routes returned
  • โœ… Turn-by-turn from an OPEN router (OSRM), not Google (2026-06-28). Google's keyless directions return abbreviated steps for longer routes - a real 6-mi route to Raising Cane's came back with 2 of ~10 turns (the early roads existed only in the map line + the via-label), which made nav skip turns and mis-place the ones it had. Routing now comes from FOSSGIS OSRM (steps=true): every turn, with street names, for drive/walk/bike - so the step list is complete and the voice says the road. Google stays for what it actually wins - live-traffic ETA (scaled onto the open route) + POIs/hours/reviews/photos. This also retired the keyless step parser as primary and the Nominatim "guess the missing road name" hack. (Open router for routing is also more on-ethos - routing is solved with open data; the FOSSGIS server is fair-use, self-host before a real release.)
  • โœ… Traffic-AWARE routing (2026-06-28) - OSRM routes by free-flow time, so it can't avoid a jam the way Google does. When Google's live-traffic route takes a meaningfully different path than OSRM's (detected by RouteGeometry.divergent, >700 m off), Vela re-runs OSRM through ~12 points sampled off Google's polyline โ†’ you follow Google's jam-avoiding route with complete OSRM street-named steps. The free-flow majority of routes stay pure OSRM (untouched, perfect turns); the snapped route leads and OSRM's free-flow options ride along as the alternates. No backend - it's the serverless answer to "use Google's smart routing": we don't always-snap because the clean tool (map-matching) is capped to 10 coords on the public server and the dense-via fallback drops ~1-in-10 turns when a via lands on one - so we snap only when traffic actually made Google diverge. The unconditional "Google routes, OSRM names turns" version waits on on-device map-matching (see below).
  • โœ… Offline routing - fully on-device + live, world catalog hosted (2026-06-30; engine replaced 2026-09-15). When you're offline (or OSRM is down), directions() falls back to the on-device obf engine (core/data/ObfRouteEngine, OsmAnd's router over downloaded per-region .obf files) and routes fully on the phone - complete street-named turn-by-turn, no signal. The first engine was GraphHopper over Contraction-Hierarchies graphs (pure JVM on ART with three workarounds, graphs built by tools/graphbuilder); it was retired on 2026-09-15 once the obf carried everything it did (the speed-limit badge and the romanized road names included) at a quarter of the download size. The old graphs are deleted on the first launch after the update with a one-time notice.
  • Get a region two ways: pick it under Settings โ†’ Offline maps โ†’ Entire states & countries (regions covering your current location sort to the top and are flagged "covers your location"; a name filter appears once the catalog is large, so a region you're traveling to - "Japan", "Texas" - is one type away instead of a long scroll), or just download offline map tiles for an area - "Download the area you're viewing" now also grabs the routing graph for the region that contains it, so one tap gives you map and navigation offline.
  • Hosting + world catalog: region files + obf-manifest.json are static assets on the obf-regions GitHub release; the catalog is tools/routing-regions.json (every Geofabrik country plus the sub-areas of the big ones) and a parallel GitHub-Actions matrix builds a group per dispatch (race-safe: per-region assets + entry artifacts, one manifest merge). (Online still wins on live traffic + POIs; this is the no-signal fallback and the foundation for offline-first navigation.)
  • โœ… Turn instructions keep the road name ("Turn right onto the local street", spoken + on the banner). Now native from OSRM - RouteGeometry.osrmPhrase synthesizes the instruction from the step's type+modifier+name+ref/destinations/exits (OSRM ships no instruction text but every step carries its road id), so there are no bare turns to fill. Highways name by ref, not name (fixed 2026-06-30): a highway step's name is empty and its identity is the ref ("I 80") + sign destinations - so road = name ?: ref and ramps read "Take exit 72B toward Richards Blvd" instead of a bare "take the exit"; Maneuver.ref also drives the banner shield even when the visible text is a name (Yolo Causeway / I 80). Validated against live OSRM. (Offline GraphHopper reads the same fields since 2026-07-13: every graph already stored street_ref/street_destination/motorway_junction per edge, so offline steps now carry shields, "toward" sign text and exit numbers too - no graph rebuild was needed.) (The retired Google-keyless path used DirectionsParser to tag-strip <step> markup and a fillTurnRoads Nominatim reverse-geocode to patch the โ‰ˆ3-of-11 turns Google omitted a <road> on - both gone now that OSRM supplies complete names; the Google markup parser only survives on the unreachable-OSRM fallback.) Regression tests: OsrmRouterTest pins the osrmType/osrmPhrase mappings; NavRoadNameTest still drives captured markup through NavEngine for the fallback path.
  • โœ… Walking / biking routes draw DASHED (Google-style) - a second line layer on the route source (vela-route-dash, round-capped short on/off pattern) toggled by visibility, because MapLibre's line-dasharray disables line-gradient (so the solid traffic-gradient driving line and the dashed foot/bike line can't be one layer). Drive stays solid + traffic-colored; Walk/Bike show the dashes. Dot rendering rebuilt 2026-07-08 (twice): dash patterns can never hold visual spacing across zoom (dash units are line-widths, the texture is quantized to integer zooms), and even MapLibre's line-placed symbol spacing stretches ~2x between integers. Final form: Vela generates the dot POINTS itself - one dot every ~17dp of screen distance along the route, recomputed as the zoom moves (0.2-zoom steps, live during the pinch via the camera-move listener; regenRouteDots in VelaMapView, capped at 3000 points) - chunky 26px SDF dots tinted like the old line, spacing EXACTLY constant at every zoom.
  • โœ… POI / interaction polish (2026-06-28): (a) ambient Google POIs now DECLUTTER - the dot layer was iconAllowOverlap=true+ignorePlacement=true, so every POI stacked on its neighbors at tight zooms; now they collide + have iconPadding, sorted by prominence (symbolSortKey), so only non-overlapping ones show and more appear as you zoom in (Google-style). (b) Tapping a POI while the directions chooser is up brings it to the FRONT (closes the chooser) instead of loading the place sheet invisibly underneath it (the sheet is gated on !directionsOpen); picking the route origin by tapping the map adopts the tapped POI as origin. (c) Search results get a clear "Hide results" bar at the bottom of the panel - the list hangs from the top so a bottom collapse control is the natural close (the top handle alone read backwards); back gesture still works.
  • โœ… Recenter fixed + photo shimmer suppressed for addresses (2026-06-28): (a) the recenter button used a target != lastCameraTarget guard that swallowed the tap when you were already "centered" then panned away (and a shown route/markers held the camera) - now a recenterTick nonce force-moves to the user once per tap, at top priority. (b) the photo-loading shimmer no longer flashes for residential addresses - it's gated on photo-worthiness (rating/reviewCount/preview present), so a bare address won't promise a gallery it'll never have (the scrape still runs silently in case it surprises us).
  • โœ… Honest remaining-distance / next-turn on routes that pass near themselves (switchbacks, cloverleaves, out-and-backs) - NavEngine now tracks monotonic forward progress along the route (windowed projection around how far you've already driven) instead of a global-nearest point, and measures both "remaining" and "distance to next turn" along the road rather than crow-flies. Before, a return leg passing a few meters from the outbound leg made the global-nearest collapse "remaining" to almost- arrived while the next turn read crow-flies-huge - the test-drive's "51 mi to turn ยท 0.3 mi remaining". (2026-06-21; unit-tested with a hairpin route - remainingStaysHonestโ€ฆ; verified on-device on a real 14.5-mi route: remaining counted down 14.5โ†’11.9 mi monotonically with 0 violations across 75 nav updates, next-turn always โ‰ช remaining.)
  • โœ… Lane guidance - real per-lane diagram (2026-06-30). OSRM gives true per-lane data (intersections[0].lanes: each lane's permitted arrows + whether it serves this maneuver), so the nav banner and the step list now draw a Google-style lane diagram - one cell per approach lane in road order, each with Canvas-drawn arrow(s) for what it allows, the lanes for THIS turn bright and the rest dimmed (NavOverlays.LaneDiagram/laneHead, Maneuver.lanes). Arrow rendering cleaned up (2026-07-02): a lane with several allowed directions now draws one shared vertical shaft + a head per direction (it used to draw a whole arrow each, double-drawing the shaft into a muddy blob and crowding the forked heads); heads are smaller and the cell wider so two heads don't overlap, dim arrows are a flat gray (not a translucent tint that built up where strokes crossed), and dim heads draw under the bright active one. On-device verified: a "Keep slight left toward I 5 South" step shows a bright straight arrow + a dimmed slight-right, with the I-5 shield. (The older count-based strip from Google's "Use the right 2 lanes" hint is kept as the fallback for the OSRM-unreachable path; the main instruction stays clean - "Turn right onto โ€ฆ".) The diagram only appears within ~0.5 mi of the maneuver (LANE_SHOW_M, 2026-07-01) - it used to render the whole time the maneuver was next, so on a long highway leg the "be in the right lane" arrows sat there for miles before the exit, which read as noise. Now they surface as you approach (Google-style); in step-preview (swiping the step list) they always show, since you're deliberately inspecting a step.
  • โœ… Spoken lane guidance - lanes-FIRST, Google-style (2026-07-03). Vela now SAYS the lane, not just draws it, and it prefaces the maneuver instead of tacking it on the end: "Use the right 2 lanes to take exit 172 toward Sacramento" (not "โ€ฆtake exit 172 toward Sacramento. Use the right 2 lanes"). It's computed from the same OSRM per-lane valid data as the arrows (Route.laneGuidance reduces them to a side + count when the valid lanes are a contiguous block at one edge; no hint when any lane works or the block is non-contiguous - the diagram covers those). Spoken once, at the first (far) prompt distance, so it doesn't nag. Localized via NavStrings.useLanesToDo - English folds it into one smooth sentence ("Use the right 2 lanes to take exit 172โ€ฆ"), other languages fall back to a safe lanes-first two-sentence form ("Empruntez les 2 voies de droite. Prenez la sortie 172") until a native grammar is hand-written. Unit-tested.
  • โœ… No more "continue on the road you're already on" - even when the name changes (2026-07-04). OSRM emits a continue/new name maneuver wherever one road flows into the next; Google stays silent there ("the name of the road does change but it literally is the same road just going straight"). Fixed at the router-mapping seam, not by heuristics in the engine: ManeuverType.CONTINUE is now minted only for "same physical road, keep driving straight" - OSRM continue/new name with a straight/absent modifier (RouteGeometry.osrmType) and the offline engine's straight-on turn (obfType; the GraphHopper-era ghType else-branch was also purified: u-turns/ferries/PT no longer masquerade as CONTINUE - a u-turn keeps its road name and would have been silenced) - and NavEngine voice-silences every CONTINUE (prompts, turn-now cue, haptics) unless OSRM attached lane guidance ("use the left 2 lanes to continue onto I-80" still speaks - the driver must position). A real bend that keeps the name (OSRM continue+left) maps to TURN_LEFT and speaks; a junction where straight is an active choice (turn+straight) stays STRAIGHT and speaks; forks/ramps/merges are structurally unaffected. The banner + step list still show every step. The first attempt (2026-07-03) suppressed CONTINUE-with-unchanged-road inside NavEngine - dead code, since OSRM never produced CONTINUE at all; the mapping-seam version is the real one. NavReplay's drive-audit exempts CONTINUE from its SILENT-turn flag so trip-log audits don't cry wolf on intentional silence. Unit-tested (mapping pins + a mock drive asserting the silent continue and the still-spoken next turn). Four more drive-reported nav fixes (2026-07-04): (1) straight rename no longer read out - OSRM stamps a few-degree slight left/slight right bearing artifact on a dead-straight new name node (Olive Dr โ†’ Richards Blvd), which slipped past the null/straight-only CONTINUE test โ†’ spoken. osrmType now treats slight-modified new name as CONTINUE too (silent); continue+slight stays a real bend (spoken) - the branch is split on purpose, and the unit pins flipped to match. (2) roundabout exit count restored - OSRM's TWO-STEP roundabout (roundabout enter + exit roundabout/exit rotary exit, the latter carrying maneuver.exit) had the exit step unmodeled โ†’ it fell to a bland "Continue onto X" ("it was considered the same road"). osrmType now maps the exit types to EXIT_ROUNDABOUT and NavStrings.phrase (all supported languages) phrases the exit number ("take exit N onto โ€ฆ"). (3) fastest route reliably leads the picker - the sort key put traffic-inflated Google alts on a different axis than un-inflated free-flow OSRM routes; now every route is normalized onto one in-traffic axis (?: durationSeconds * gRatio) with a provisional/named tie-break. Second hole closed 2026-07-08: naming a picked (or top-sorted) provisional alternate re-snapped it through OSRM and swapped in a RECOMPUTED ETA in place, so a row's time could leapfrog its neighbors after the sort ("Fastest" tag below a slower first row). nameRoute now keeps the route's original Google duration/in-traffic figures; the snap contributes geometry, named turns and congestion spans only. And the tag is singular now (2026-07-08): a route under ~30 s slower rounded its delta to 0 and ALSO wore "Fastest" (two tags, different displayed minutes); only the top row gets the tag, near-ties just show their ETA. (4) compass no longer buried under the nav card - MapLibre's top-right compass is dropped ~112 dp below the top during nav so the full-width maneuver banner stops painting over it. (Roundabout phrasing + route-sort both want one real drive to confirm against live data.)
  • โœ… Nav survives a process kill - "Resume navigation?" (2026-07-04). On GrapheneOS the Android-14 FGS-location restriction can stop the foreground service from holding the nav process alive, so the OS reaps it mid-drive and you reopen into plain browse mode (route line gone, arrow pointing by compass instead of heading-up - a real drive report). Fix: startNav persists the destination (+ label/mode) to a vela_nav_resume pref; on the next launch MapViewModel.maybeOfferResume raises a "Resume navigation to <place>?" card (Resume / Dismiss) if that drive is recent (RESUME_MAX_AGE_MS = 60 min), and Resume re-routes from your CURRENT fix to the saved destination and restarts nav - a fresh route that accounts for however far you drove while the app was gone (and any traffic since), rather than restoring a stale line. Cleared on stop AND on arrival (the arrival branch now clears the resume pref too, so a completed drive isn't offered for resume next launch - audit 2026-07-06). The 60-min freshness window is heartbeated every 5 min while driving (NAV_HEARTBEAT_MS), so it measures time since the interruption, not since nav start - a >60-min drive can still be resumed (before, the timestamp was written once at start and a long drive fell outside the window). Only the destination is persisted (no Route serialization). Device-verified on a Pixel 5a: start nav โ†’ am force-stop (simulating the reap) โ†’ relaunch โ†’ the "Resume navigation to Safeway?" card appears; Resume re-routes + resumes. (The other restart symptom - the map not re-plotting a still-live route on a mere Activity recreate - is moot here since the session is rebuilt from the current fix.)
  • โœ… Navigation tracking overhaul - the standstill/progress glitches (2026-07-04). Four field bugs from real drives ("mph keeps going when stopped", "progression halts as if I'm not moving", "turns showing up 12 miles early"), all traced and fixed at the mechanism: (1) GPS starvation at a standstill - LocationProvider registered with a 2 m distance filter, so a stopped car got NO fixes: the speedometer froze at the last braking speed, the puck's Kalman never measured 0, and the dead-reckoner crept the puck forward on the stuck speed; pulling away then FROZE the puck (real position behind the crept target โ†’ every fix rejected as backward) until the car re-drove the phantom meters. Fix: minDistance 0 m (~1 Hz dopplerโ‰ˆ0 fixes while parked - the readout and model settle by themselves), plus belt-and-braces: a 3 s no-fix zeroer + a 3 s speedless-fix evidence timeout (SPEED_ZERO_MS/SPEED_HOLD_MS), the Kalman now measures ONLY each fix's own doppler/derived speed (mySpeedRaw - feeding it the held display speed re-injected the stale value at near-unity gain), decays toward 0 during outages (SpeedKalman.decay, ฯ„=4 s), and the spike filter only applies across โ‰ค3 s gaps (else the post-tunnel 29 m/s doppler is rejected against a zeroed 0+15 forever - caught in adversarial review before ship). (2) Parked-jitter ratchet - the puck's monotonic targetM accepted every forward GPS wobble at a red light. Now gated at near-zero modeled speed (noise floor 8 m), and the 2 s dead-reckon window re-anchors only on ACCEPTED fixes (a rejected wobble used to re-arm the blind window every second). (3) Mid-drive freezes - three unfreeze paths: a persistent over-cap forward step is accepted on the 2nd consecutive occurrence (post-gap catch-up used to deadlock against a stale plausibility cap; isolated multipath spikes at a light DON'T count - the streak resets on every normal fix), snap misses disengage at 3 (not 6), and the snap tolerance scales with speed (22โ†’35 m - OSRM geometry sits half a road-width off the driven lane on divided roads) with the stale-bearing heading gate skipped when stopped. The look-ahead window is sized from the speed at the last accepted fix, not the decaying model. (4) "Turns 12 miles early" - the out-and-back wrong-pass bug. On a route that reuses the same asphalt (out-and-back, divided highway, cloverleaf), NavEngine's GLOBAL maneuver projection matched a return-leg turn onto its outbound twin: the moment it became the target, its along-route distance read โ‰ˆ0 and it announced/advanced miles early. Maneuvers are now located by a prefix-sum of step lengths refined by a ยฑ800 m windowed projection (maneuverAlong), and off-window re-acquire prefers the pass nearest current progress (projectNearAnchor, 2 cm/m along-distance penalty - a far leg can never outbid a valid near match inside the 60 m acceptance band). For this to work the via-stitched routes (traffic snap, picked alternates, multi-stop) had to keep step lengths TILING the polyline - the via filter now folds dropped boundary-step distances into the last kept maneuver (they used to vanish, drifting every later estimate km short; also caught in adversarial review). Unit-tested with synthetic out-and-back routes (wrong-pass distance, near-pass re-acquire) + replays feed the puck Kalman again (mySpeedRaw in the replay collector).
  • โœ… Full navigation audit + remediation (2026-07-04). Before the next real drive, a 6-lens audit (position/reroute ยท speed chain ยท route-line rendering ยท banner/prompt timing ยท engine/session ยท a completeness critic, each benchmarked against OsmAnd/Google behavior) produced 39 findings; the confirmed ones were fixed in one pass and adversarially re-reviewed. The big ones: Position integrity: NETWORK (BeaconDB) fixes are no longer trusted blindly - dropped outright during nav, and in browse only used when GPS has been quiet โ‰ฅ12 s (OsmAnd's useOnlyGPS discipline); they were routinely 100-1000 m off and teleported the dot onto parallel streets โ†’ spurious reroute โ†’ teleport back โ†’ reroute again ("GPS thinking I am somewhere else"). Inter-fix dt now uses the monotonic elapsedRealtimeNanos (mixing GNSS UTC with network system-clock stamps made dt<0, which bypassed the outlier filter entirely - a one-fix teleport). Coarse fixes (accuracy >50 m) update the dot but never steer guidance. Rerouting: a failed reroute fetch used to KILL rerouting for the rest of the drive (the off-route event is edge-triggered and the latch never cleared) - failures now clear the latch so 4 more deviated fixes naturally retry (~4 s cadence), with single-flight + a 10 s adoption cooldown + "Rerouting" spoken at most every 30 s (no more reroute storms), a session-generation stamp (a late reroute can't resurrect the previous destination's route into a new session), a route-identity guard on the per-fix state write (the old-route progress can't be written onto a freshly swapped route โ†’ no false arrival after a reroute), no reroutes while provably stationary (red-light multipath drift), no reroutes within 150 m of the destination, and off-route now measured against the SAME windowed projection progress uses (being near the RETURN leg of an out-and-back no longer masks a genuine exit). Guidance timing: prompt distances scale with speed - max(400 m, vร—25 s) far / max(150 m, vร—8 s) near / turn-now (vร—2.5 s, 25-90 m); the fixed 400 m gave a 75 mph driver 12 s to cross three lanes (Google: ~2 mi + 1 mi + 500 ft). City/walking behavior is byte-identical. Short steps speak ONE prompt with the TRUE distance ("In 50 meters, turn left" - it used to fire both bands back-to-back announcing the literal thresholds). After a GPS gap, maneuvers passed in the tunnel advance SILENTLY (each used to replay as an at-the-turn command + firm haptic, one per second). Arrival gets an approach cue ("In 400 feet, your destination will be ahead" - new NavStrings.destinationAhead, all supported languages) and fires on PROXIMITY too (crow โ‰ค40 m of the snapped endpoint, or stationary within 50 m remaining - parking short used to never arrive, then "Rerouting" fired in the lot). Lane arrows + the compound "then" row gate on a speed-scaled approach distance (~30 s; the "then" row used to sit on the banner for 12 km when a merge followed a far exit). The banner seeds the first turn's distance at start (no more "0 ft" flash), and a previewed step's headline now shows its APPROACH distance, not the leg after it. ETA: remaining time the remaininSTEPS' durations (pro-rated current leg, ร— the live-traffic ratio) - it used to divide remaining distance by the whole-route average speed, reading "4 min" for a 15-minute downtown tail and poisoning the faster-route comparison. Faster-route offers also remember a dismissal (no re-speak of the same candidate every 2 minutes) and never re-fetch while one is on screen. Speed chain:** the spike filter is now symmetric + cel-bounded (|ฮ”v| โ‰ค 8 m/sยฒยทdt + slack) against the last ACCEPTED measurement with a 2-fix persistence escape - the old one-sided +15 m/s check let a doppler down-glitch to 0 through at 67 mph and then rejected every real 30 m/s fix against the held 0 (speedo latched at 0 until you slowed below 33 mph); shared with the replay path (which had NO escape - one recorded glitch zeroed a whole replay). The no-fix zeroer moved 3 sโ†’6 s (it fired between normal 3 s canopy fixes: 56โ†’0โ†’56 flicker), the dead-reckon window 2 sโ†’3 s (glide-stall-lurch at canopy cadence), the DISPLAYED speed is smoothed with a stop deadband (raw 1 Hz doppler flickered 59/60/61 at cruise), and speed derivation requires two GPS fixes past an accuracy-scaled floor (a BeaconDB hop minted phantom 16 mph readouts at red lights). No more route "appendix", second pass 026-09-06): the shape itself is now detected: a a route that keeps traveling while making no progress along Google's line (an off-ramp and back, where the snap distance is tiny and the extra length is just a ramp pair) is refused too. No more route "appendix" (2026-09-06):** whethe open router is led along Google's course through sampled points, a point that snapped onto a side road used to produce a spur out and back that the arrow then drove while the car went straight. A via route is now refused when any sampled point snapped more than 40 m from where it was asked for, or when the result is markedly longer than the course it was meant to follow; the plain route is used instead. Wrong turns read faster (2026-09-07):** a diberate turn off the route now asks for a new route on the second fix after the turn instead of the third, and the arrow stops gliding along the old line the moment your course leaves it, dropping to your real position within two fixes instead of coasting straight through the corner for three seconds. Road behind you, off: smooth (2026-09-07):** theine behind the arrow now clears in a continuous edge under the arrow instead of 12 m chunks with a dithered fringe. Road behind you, off: no stub (2026-09-): with the trail off, the line behind the arrow us to reappear as a growing blue stub that vanished in a chunk every 300 m. The cut is now carried by the ahead line's own gradient in that mode, so nothing is drawn behind the arrow at all. Avoid tolls and highways work online (20-09-06): Google's keyless directions do honor the avoid tions after all (the flags were found by capturing Google's own web client), so with either toggle on the route follows Google's avoiding course with the open router's named turns and Google's live-traffic time. Alternates are avoid-aware too. A downloaded region is no longer required; the on-device router only steps in when Google is unreachable, and only then does the "may still use tolls and highways" note appear. Avoid ferries (2026-09-16, issue546): a third sticky chip beside Avoid tos and Avoid highways, in both route choosers, drive only. Online it rides Google's keyless directions the same way (the ferry flag was captured from Google's own web client); offline the downloaded region's router skips ferry crossings. The note under the chips now names ferries too. Roundabouts say "take the 2ndxit" (2026-09-05): English used "take exit 2", whicthe spoken "take exit N" rewrite (an espeak workaround for motorway exits) turned into "take the 2 exit". Now ordinal like every other language, and a straight-through roundabout is named as such: "go straight through onto 13th St, the 2nd exit". Road-ahead bar, second cut (2026-09-04):** reawn to the TomTom RouteBar the #228 reporter attached. A track from the arrow (bottom, with the remaining trip distance) to the top of the look-ahead window (labeled with the distance it spans), congestion as red/amber segments, and what is coming as round icon badges: cameras, level crossings, speed humps. Lights and stop signs stay small dots because a town has dozens in a few km. The first cut's 3 dp ticks were unreadable. Current road name, where you want it (2026-09-04): Settings > Navigation > "Current road name": above the bottom bar (default, always centered, long names fit), under the arrow (issue #288's mockup), or off. The nav bar has a chevron handle that both hints at the swipe and is a real button (tap, focus ring, OK). Settings > Navigation > "Prefer buttons over swipes" keeps the separate step-list button beside it; keypad-first phones get that button automatically. Swipe up for the steps (2026-09-04): the ETA bar at the bottom of the nav screen is the handle for the step list, Google-style. Drag it up (it lifts with the finger) or fling it and the step sheet slides in from where the bar was; swipe the sheet down and the bar is back. The list button is still there and is the tap and D-pad path, so keypad phones lose nothing. Road behind you (2026-09-03): the driven part of the route no longer trails behind the arrow by default; only the road ahead is drawn (Google's current look). Settings > Navigation > "Road behind you" brings the gray trail back. Implemented as paint: the driven color in the route gradients becomes transparent and the full gray line is hidden, so nothing is re-uploaded. The puck sits still (2026-09-03): in follow mode the arrow is drawn as a screen overlay at the projection of its smoothed point through the camera state just set, instead of as a map symbol whose per-frame update could land a frame late. Measured on a straight highway, the chevron moved 1 to 2 px on 95% of frames before and 0.07 px on average after. Same look: rotated relative to the camera, foreshortened by the tilt. Route line ("the gradient when you zoom in"): the driven/ahead cut is now a GEOMETRY split - a traversed-gray full line (theme-aware, dimmer than the alternates' gray) under an ahead-suffix layer cut exactly at the puck, with traffic spans remapped onto the suffix. Since 2026-09-03 the moving cut itself is paint, not geometry: a short overlay piece's line-gradient (1.6 m per texel) carries the gray/color boundary and slides forward every ~300 m, so nothing re-tessellates per frame or on a timer (the old 150 ms re-upload dropped a map frame at a steady 6.7 Hz for the whole drive). The old line-gradient stop could never be crisp: MapLibre bakes gradients into a 256-texel texture, smearing any "hard" cut into a routeLength/256-meter ramp (~39 m on a 10 km route, hundreds of px zoomed in) - and MapLibre has no line-trim-offset, so geometry is the only exact cut. Route geometry also uploads only when it CHANGES (it re-tessellated thousands of vertices on every recomposition, ~1-2ร—/s during nav). Voice/audio: audio focus is refcounted per speech burst - queued prompts used to leak one request and abandon the other mid-sentence (music un-ducked OVER "Turn right onto Main St"), interrupts stranded the count (no onStop override), and there was no focus-change listener at all (guidance talked over incoming calls; it now stops speaking when a call takes focus). Session lines ("Rerouting", the faster-route offer, the stops notice) now speak in all supported languages via NavStrings. Steadier ducking (2026-07-04, user: "not ducking enough, and it didn't reliably duck every time"): the root cause was flapping - focus was abandoned the instant each prompt ended, so the driver's music snapped back to full volume between (and within) closely-spaced prompts, reading as weak/unreliable ducking. Fix: a ~1.5 s focus-hold on release (FOCUS_HOLD_MS, Handler-debounced) instead of abandoning immediately - a compound prompt ("In 500 ft โ€ฆ turn right") or an interrupt flushing the previous one now keeps the audio ducked continuously across the gap; a new prompt within the window reuses the still-held focus (device-verified: two back-to-back prompts made ONE focus request, not two + an abandon). The request result is checked (focusHeld) so a FAILED grant is known rather than speaking over full-volume audio blind. Stays on AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK (OS-managed duck + auto-restore): a brief detour to plain GAIN_TRANSIENT (pause the music entirely, for a deeper drop) was reverted - many players pause on transient loss but don't reliably auto-resume when focus is handed back ("Vela paused the music and didn't restart it"). Duck DEPTH is set by the OS/player, not tunable via the focus API; the continuous hold is what actually makes it read as ducked. Lifecycle/UX: arrival tears the foreground service down (dismissable notification; the location-typed FGS + ongoing notification + 1 Hz GPS used to run FOREVER if you pocketed the phone without tapping Done), and a "Searching for GPSโ€ฆ" chip (all supported languages) shows during nav when fixes stop - the banner used to freeze with a confident blue arrow and zero indication. RouteGeometry.reposition also placed every maneuver one full step late (off-by-one vs the placeManeuvers convention) - fixed. Unit-tested (NavAuditRegressionTest: speed-scaled prompts, single-prompt short steps, silent catch-up, proximity arrival, stationary-drift no-reroute, per-step ETA, arrival approach cue). A second adversarial round then broke the first fixes and 10 more corrections landed before ship - the important ones: the destination-zone reroute guard is measured by CROW distance with a deferred-fire flag (guarding on the frozen remaining made a turn missed near the destination a PERMANENT silent limbo - frozen banner, no reroute, no arrival, while the driver left the area); the stationary floor is MODE-AWARE (2 m/s classified every walker as parked โ†’ pedestrian rerouting dead + 50 m-early walking arrivals); the failed-reroute latch clear moved onto the location thread (the coroutine write raced the in-flight fix frame); PiperSynth's abort paths double-fired onDone (double-decrementing the focus refcount - music un-ducked over the interrupting prompt); NETWORK fixes may paint the DOT after 12 s of GPS silence even during nav (guidance stays GPS-only - a GPS-less phone deserves a coarse position, not a dead app) and the GPS chip also fires when guidance is starved by sustained 50-80 m accuracy (coarse fixes kept the stale timer from ever firing); the split re-upload gained a 150 ms wall-clock floor; a mid-nav reroute seeds the ahead layer immediately (no gray-route flash); the arrival notification detaches BEFORE posting (else it stays non-swipeable) and Done clears it. (Deferred, documented: backward-driving/U-turn detection, bearing-biased reroute origins, adaptive dead-reckon cadence, raw-vs-raw movement accumulation for speedless-chipset walking.)
  • โœ… Trip replay made trustworthy: segments, hermeticity, real-time-scaled puck (2026-07-04). Auditing the user's actual glitchy drive (TripLog.audit on the shared CSV) found the replay system itself was manufacturing most of the on-screen chaos: (1) Franken-routes. A trip records every route the drive used (start + each mid-drive switch as its own RP/RD/M block) - but the parser grabbed the FIRST polyline and EVERY maneuver line in the file, so a trip with a route switch replayed as one stitched route with two DEPARTโ†’ARRIVE runs: mid-replay "arrival", card distances km wrong, the arrow matched against the wrong half. TripLog now parses segments (each block + the fix index it activated at), audit() replays each segment against exactly the fixes driven on it, and the in-app replay swaps routes at the recorded fix positions. Live drives now also RECORD reroute/faster-route swaps as new blocks (previously only a manual nav restart wrote one). (2) Live fetches during replay. Replays ran the real reroute + faster-route recheck - a wrong turn in the trace fired a LIVE OSRM fetch and swapped the route mid-replay (matching the trace against a route never driven), and the recheck popped the faster-route sheet over a replay. NavSession.replayMode makes replays hermetic: no fetches, recorded swaps only. (3) The 3ร— stutter. Replays play 3ร— real time but the puck's dead-reckoning, blind window, easing time-constants and plausibility caps all ran on the WALL clock - the puck covered โ…“ of the ground between fixes and surged to catch up every fix ("stuttery arrow, pulsing mph"). The map view now runs those clocks in trace time (replaySpeedup; live = 1 โ†’ formulas byte-identical), closing the long-deferred nav-camera-replay item. Bonus (benefits LIVE nav too): maneuver positions are now resolved sequentially - each maneuver projects onto the polyline strictly FORWARD of the previous one (the stopMarks technique) - replacing the step-length prefix sums entirely. Exact on reused asphalt by construction and independent of step metadata (old recordings carry unfolded via distances; Google's fallback steps are abbreviated - both used to skew the estimates). The ETA pro-rate uses the resolved geometric leg length for the same reason. Also fixed: the documented -DvelaTrip audit harness had NEVER worked (the Gradle daemon property never reached the forked test JVM - the test silently skipped); core/build.gradle.kts now forwards it. Audit of the user's trace after the fixes: both segments track cleanly; the residual flags are artifacts BAKED INTO the old recording (raw BeaconDB teleport fixes the old pipeline recorded, and a second route computed from a minutes-stale origin) - the new live pipeline neither records nor feeds those.
  • โœ… Compound maneuver preview ("โ€ฆ then keep right") - the banner's secondary "then <next>" line now shows only when the next maneuver closely follows this one (COMPOUND_M โ‰ˆ 0.3 mi, isCompoundNext, 2026-07-01), the way Google surfaces back-to-back turns (exit-then-merge). It used to show for any next maneuver, even one miles away - the same noise as the lanes-too-early bug. The "then" line now also carries the next step's highway/exit shield, so a compound "then take I-580" shows the I-580 shield.
  • โœ… Highway/exit signage with real shield shapes - route refs ("I-80 E", "US-50 E", "CA-99", "ON-401") and exit numbers ("Exit 4A") are parsed out of each instruction and rendered as Google-style badges: a green exit tab plus the actual route-shield shapes (2026-06-27) - a red-top/blue interstate shield, a white US-route shield (both drawn as Compose Canvas paths, real-signage colors, light/dark-independent), and a neutral white marker for state/provincial routes. The network is inferred from the ref prefix + a state/province set (parseRouteRef, unit-tested) - I โ†’ interstate, US โ†’ US route, a 2-letter state/province code โ†’ state - with no OSM lookup, per the design call. Anything unrecognized falls back to the plain bordered chip. (v1 draws one neutral marker for all states/provinces; per-state shapes - a California spade vs Ontario's crown - ride OSM Americana's set as the follow-up. Shown in both the nav banner and the step list.)
  • โœ… Route geometry via open router - per-mode FOSSGIS OSRM backends (routed-car/routed-bike/routed-foot), so drive/walk/bike each follow the correct network; Valhalla later
  • โœ… Live route re-check while navigating - every ~2 min underway Vela re-queries traffic and offers a faster route when one appears (NavSession recheck; see Navigation below)
  • โœ… Walking + cycling modes (drive/walk/bike) - each with its own path-following line, not a car route reused
  • โœ… Public transit directions - a Transit chip in the directions chooser shows a Google-style results board: each option's departureโ€“arrival window, total duration, distance, agency, and the colored line pills you ride (real Google line colors + per-mode glyph: ๐Ÿš† train / ๐ŸšŒ bus / ๐ŸšŠ tram / โ€ฆ). Like photos, transit is served only to a real browser engine (a plain GET with the !3e3 flag is silently downgraded to driving), so it goes through a hidden WebView (app/web/WebDirectionsFetcher) that loads the /maps/dir/โ€ฆ/!3e3 page and reads the itinerary set out of APP_INITIALIZATION_STATE (the longest )]}' payload at slot [3]); TransitParser (keyless) parses it. Verified on-device Davisโ†’Sacramento (6 options: Amtrak Thruway, Yolobus 42B/43/44, โ€ฆ).
  • โœ… Transit "departs in X min" countdown + live badge (2026-07-12). Each board option leads with a Google-style countdown ("Departing" / "in 7 min") computed from the parsed departure epoch against a shared clock that ticks every 30 s; options departing more than 90 minutes out (where the printed time carries it) or already gone are left unmarked. When the itinerary carries real-time data (a leg with a live delay, or a real-time time that differs from the timetable) the countdown reads green with a Live dot, and the boarding leg's "N min late/early" is surfaced right in the header (it was only in the drill-down before). Pure render off already-parsed fields (TransitItinerary.departureEpochSec, TransitStep.delayText), no extra fetch; localized in all supported languages.
  • โœ… Canonical transit stops on the map (2026-07-13, Transitous, device-verified). At street zoom the bus stops you see are the agencies' own GTFS stop positions from Transitous (map/stops per viewport, area-cached), drawn as a blue bus badge with the official stop name - one icon per station (bays collapse onto their parent). Tapping a stop opens its live departure board instantly by stop id - no Google lookup, no name matching, so intersection-named stops and multi-bay hubs just work. Where this layer has coverage the OSM basemap's bus icons hide (no doubled stops); rail/airport icons stay basemap. Stops keep working offline: every area you browse online is cached to disk (24-area LRU) and redraws with no signal - the same continuously-fresh property as the surveillance-camera dataset; never-visited areas fall back to the OSM icons. Directional curb pairs merge into one icon (2026-07-13, device-verified). US feeds name both sides of the street identically with no direction field, so the same intersection used to draw two overlapping same-named badges. Same-named stops within 160 m now collapse to a single icon at their midpoint, and its board merges both curbs' departures - the headsigns distinguish the directions (one row heads to the regional hub, another to the opposite terminus), the way Google presents an intersection stop. Transit directions are unaffected: they already walk you to the exact boarding coordinate of the specific curb. Stops whose names carry the direction (NB/SB station styles) never merge.
  • โœ… Same-business rule in every app language (2026-09-22). Descriptor words, legal forms and street abbreviations for all 13 languages are read as descriptors whatever the phone's language, accents fold across scripts, and Chinese, Japanese, Korean and Thai names compare as strings with their suffixes (ๅบ—, ่–ฌๅฑ€, ้Š€่กŒ, ์ง€์ ) stripped. The bake's word list carries the same union.
  • โœ… Same-business rule, city pass (2026-09-22). Brand prefixes ("Bank of America Financial Center" / "Bank of America ATM"), phrases inside longer names ("23rd Street Dental"), plurals, "Dr."/"Doctors", street words no longer counting as identity, and neighborhood words shared across the places on screen treated as generic. Checked against Google over Midtown Manhattan (81% of Google's places linked) and downtown Houston (83%).
  • โœ… One same-business rule for taps, twins and the bake (2026-09-21). Descriptor tails ("Circle K | Gas Station", "U.S. Bank Branch", "CVS" vs "CVS Pharmacy"), spelling variants ("&"/"and", "St"/"Street", accents, "DDS Inc.", "by Wyndham") and agreeing identifying words now read as one business, while shared generic words ("Russell Park Apartments" vs "Orchard Park Apartments") no longer do; the bake keeps one row per business (a gas station listed twice, a store and the counter inside it). Derived from a Google-versus-archive study over the Davis fixture, then Midtown, Houston, Berlin and Tokyo: one generic table per app language, legal forms and street abbreviations per language, CJK names compared as strings, Europe's four-letter brands, glued names, and a tap on a label in another script (a Japanese map on an English phone) that looks the listing up in the label's own language when the English answer does not name it.
  • โœ… Offline maps page: Downloaded list + alphabetical country tree (2026-09-22, #601). What is on the phone (saved areas and installed regions) sits right under the storage figures and the delete button; the catalog is one A to Z list where a split country (Germany, Canada, the United States with its states and territories) is a row that expands to its pieces and downloads them all in one tap.
  • โœ… About says who installed Vela (2026-09-22). The version group shows the install source, so a phone set up for Android Auto through King Installer or AAEnabler can see that the Play install source is still in place before and after an update.
  • โœ… Offline departure board (2026-09-21). Every board fetched is kept on the phone, so a stop tapped with no connection shows the routes, headsigns and colors it had last time with a "last seen" line instead of nothing (TransitBoardCache).
  • โœ… Live stop departure board (2026-07-12, keyless + device-verified). The board is ownership-gated (2026-07-16): it renders only on the place it was fetched for, so a previously viewed stop's departures can never linger onto an unrelated place (a saved/recent place opened right after a stop used to show the stop's board). Tapping a transit station (subway / rail / BART / bus stop) shows Google's "See departure board" right in the place sheet: each line and direction with its route number in its real line color (the "14"/ "38AX" pill), its destination/headsign, the next few departure times (soonest bold, the rest quiet), a countdown on the soonest ("in 6 min", green + a Live dot when Google has a real-time fix), and the running frequency. Handles both layouts - a station grouped by line/direction, and a busy bus stop that lists departures flat (the parser groups those by route so route 14 is one "14" row with its next times, not 25 rows), soonest-first. It rides the SAME anonymous WebView + APP_INITIALIZATION_STATE channel as transit itineraries and photos - the board is embedded in the station's own place page (no separate RPC, no login; unlike popular times it survives a logged-out session), parsed keyless by :core's StopDeparturesParser (place[62], shape-tolerant leaf matching, unit-tested against a live NYC capture) via WebStopDeparturesFetcher. Gated to transit-category places so it never fires on an ordinary business. Device-verified: BART Powell St rendered 8 lines (Daly City / Richmond / SFO-Millbrae / Antioch / SFO) with their next departures. Coverage is agency-dependent - major systems that feed Google real-time carry the board (NYC MTA, SF BART confirmed); a place with none shows nothing (a small light-rail agency like SacRT was confirmed empty, correctly). Localized in all supported languages.
  • โœ… Tap-through route stop timeline (2026-07-12, keyless + device-verified). Every route on the departure board is tappable (a > chevron hints it): tapping one opens a full-screen stop timeline for that route - the board stop, every intermediate stop with its call time, and the alight stop, drawn down a vertical rail in the line's own color (board + alight bold). Tapping any stop on the timeline opens that stop's own departure board, so you keep drilling down the line the way Google's tap-through does. No new endpoint: the stop sequence is a lazy fetch not in the place blob, so it reuses the proven transit-itinerary parser - a keyless directions query from the tapped stop toward the route's headsign returns a ride leg whose intermediateStops already carry the per-stop times. The headsign geocode is biased toward transit terminals (a bare "Richmond" would otherwise resolve to a city district, not the BART terminal), and the leg we actually board at this stop is chosen. Best-effort: a headsign that will not geocode falls back to a quiet "Route details unavailable". Device-verified: BART Powell St -> Yellow-S to SFO showed all 11 stops (Powell -> Civic Center -> 16th/24th St Mission -> Glen/Balboa Park -> Daly City -> Colma -> South SF -> San Bruno -> SFO) with times 12:23-12:54 PM, and tapping 16th St Mission opened that stop's own board. Localized (route_detail_unavailable) in all supported languages.
  • โœ… Transit leg drill-down with full stop detail - tap an itinerary to expand its ordered legs. A ride leg shows Google's whole layout: the line pill + "towards โ€ฆ" headsign, the board stop (name + agency Stop ID + real-time board time, with "N min late/early" when it drifts from the timetable), an expandable "N stops" list of every intermediate stop with its time, the alight stop, plus the trip's service alerts (detours/info) and a "Tickets & info" footer with the agency name + a dialable phone (and fare for agencies that provide one - many US agencies don't). All of it parsed (unit-tested) from trip[1][0][1] in the same keyless fetch - no extra RPC. Calibrated + device-verified Miamiโ†’Aventura Mall (route 9, 2026-07-07); single-digit route numbers ("9") now render a pill (the โ‰ฅ2-char guard dropped them).
  • โœ… Transit itinerary drawn on the map (2026-08-08, issue #233) - expanding an itinerary in the chooser draws its whole trip on the map, Google-style: each ride leg as a line in the agency's own color running through every stop (white dots along the way, bigger ones where you board and alight), walk legs as dotted links, and the camera frames the full trip. Collapsing the row or switching modes clears it.
  • โœ… Transit walk-leg turn-by-turn - tap a Walk leg in the drill-down to expand turn-by-turn walking directions between that leg's endpoints (the previous ride's alight stop / trip origin โ†’ the next ride's board stop / trip destination). The endpoints come from the stop coordinates in the same keyless transit payload (TransitParser now reads each stop's [4][2]/[4][3] and assignWalkEndpoints wires the adjacent stops onto each walk leg); the steps are fetched on demand via the normal walk router (OSRM foot) - no extra transit RPC. Collapsed by default; a chevron opens it and a spinner shows while the route loads.
  • โœ… Transit step-by-step guidance (Moovit-style) - an expanded itinerary gets a Start button that opens a leg-by-leg guide: the current leg large (with its walk-directions drill-down), the remaining legs as a timeline, and Back / Next controls. Since 2026-08-08 (issue #232) the guide is a bottom pane with the live map above it: the whole trip stays drawn (colored ride lines through the stops, dotted walks) and the camera frames the leg you are on, re-framing as you advance - so you can see where you are along the route, not just read about it. It advances automatically as GPS reaches each leg's end (board/alight stop or the walk destination) and speaks each instruction ("Take Route 9 towards Aventura, get off at โ€ฆ", "Walk 7 min", "You have arrived") through the selected voice, localized in all supported languages. The auto-advance is latched (MapViewModel, TransitNavState): a leg must first be ARMED by being >90 m from its end, then advances on entering the 40 m radius - so a transfer hub with two leg-ends close together can't cascade and a short final walk can't fire a premature "arrived". Back exits guidance.
  • โฌœ Per-minute predictive future-traffic ETA (login/app-only - keyless gives the typical range only, see Depart/arrive time above); avoid tolls/highways
  • โฌœ Self-hosted routing backend (replace the FOSSGIS community server)
  • โœ… Pause and mute are one button (2026-09-18). The two controls that hold something about a drive shared a tall pill down the right edge; now they share one 56 dp button. The tap pauses, so the thing you reach for at speed still costs one touch; mute slides out beside it for a few seconds in case that is what you wanted, and a long press mutes outright. The button says what is going on without being touched: the glyph is pause or resume, it fills with the accent while the drive is held, and it wears a small crossed speaker while the voice is off.
  • โœ… The in-drive stop card says what the stop costs, marks it on the map, and takes itself away (2026-09-18). Tapping a place while driving (Settings > Navigation, off by default) already offered it rather than acting on it; the offer now carries the three things that make it usable at speed. It prices the detour: one route through the candidate, fetched in the background and bounded at 8 seconds, compared with the drive's own live remaining time, shown as "+4 min" on the card. The candidate is priced where the button would actually put it, first in the stop list, and a difference too small or too large to mean anything is not shown at all rather than rendered as "+0 min". The card marks the offer on the map with a red "+" pin, so it is obvious which place is being offered. And it dismisses itself on a countdown ring drawn around its close button, ten seconds for a thumb and twenty-five on a key-driven phone, restarted by a second tap on the same place - an offer nobody answers no longer sits over the map for the rest of the drive. Nothing about the drive changes until the button is pressed, and the pricing fetch never touches the running session.
  • โœ… The nav puck glides on kinked road geometry (2026-07-25). The drawn position is a real boxcar average across a speed-scaled along-route window and the arrow's heading is the chord across that window, so the lane-level micro-kinks in dense OSM geometry cancel instead of wobbling the whole heading-up map like a record needle. Corners round by only a couple of meters at speed, same as Google's puck; no temporal lag was added.
  • โœ… Spoken turns echo as a visible heads-up while the app is backgrounded (2026-07-25). The ongoing nav notification stays silent and minimized, but when guidance speaks with no Vela screen visible, a self-expiring heads-up pops with the same arrow, distance and ETA line, on a separate high-importance channel that is explicitly soundless and vibration-free (the voice is the audio). Foreground drives and muted drives stay exactly as they were.
  • โœ… Picture-in-picture mini map while navigating (2026-07-25). Leaving the app mid-drive shrinks Vela to a floating window with the live map, route line, puck and a one-line distance-plus-turn strip, Google style. Android 12+ auto-enters whenever navigation runs; older versions enter on the home gesture; expanding restores the full interface. Launchers that forbid picture-in-picture keep the notification-only background nav.
  • โœ… Steps viewer: swipe down anywhere to close + no stray stops while it's up (2026-07-15, user). The route step list now dismisses with a downward swipe on the BODY once the list is at its top (nested-scroll into the same drag the handle uses; mid-list swipes still scroll), not just via the X. And the route-through-here map press is gated: while the steps viewer or the full route picker covers the map, taps on buildings/unnamed POIs (which share the long-press handler) no longer silently add a stop and reroute - adding a stop by pressing the map works only once the chooser is minimized to its Start bar, where the map is the primary surface. Deliberate flows (Add stop -> choose on map, the origin picker) are untouched.
  • โœ… Compact turn banner on short screens (2026-07-15, ported from the vela-dpad fork, credit ars18). Below 500dp of screen height (landscape phones, car head units in landscape) the maneuver banner shrinks its paddings, glyph and distance type so it stops eating the map; ordinary portrait phones are byte-identical.
  • โœ… "Take the ramp" pronounced right (2026-07-11, ear-verified). espeak's phonemizer mis-voweled "take" ("tyke") when the whole ramp sentence was phonemized in one breath; the spoken text now inserts a comma before "toward", so the maneuver clause and the sign destination are separate beats (Google pauses there too). Banner text unchanged.
  • โœ… Car screen round three (2026-09-22, awaiting a head-unit check). Pause/Resume from the car with a "Paused" card, search along the route (fuel, food, coffee, the phone's quick categories) with a pick becoming the next stop, camera/speeding/closing-soon alerts as car toasts, a "Continue on " card while the next turn is far, an overview toggle, and the route's lights, stop signs, speed cameras and plate cameras drawn on the car map.
  • โœ… Car screen round two (2026-09-21, awaiting a head-unit check). Vela's own map theme on the car map, the library's multi-source watermark replaced by one OpenStreetMap credit, the puck framed inside the visible area (it hung off the bottom edge), the phone's smooth between-fix glide and an eased zoom instead of per-fix snaps, and Vela's voice for drives started from the car.
  • โœ… Car-screen puck sized to the screen (2026-09-21). A fortieth of the short side instead of a fixed 22 px radius, which was a fifth of a 480 px head unit's height.
  • โœ… Android Auto, full car-side navigation (2026-07-08, PR #17 by jacobjeger; replaces the first cut). Vela registers as a navigation-category templated car app (sideloads appear once AA's developer "Unknown sources" switch is on). The car now runs the WHOLE flow by itself: a home screen (Home/Work, recents, saved), car-side search (debounced, biased to your location), a route preview with up to 3 live-traffic alternates, and active turn-by-turn with the turn card, a "then" step, the lane diagram, a faster-route action, mute, and instrument-cluster support (NavigationManager navigationStarted/updateTrip - without which the old cut never showed a turn card). Rendering moved off the fragile VirtualDisplay+Presentation hack onto MapLibre's supported MapSnapshotter (bitmap โ†’ car surface) with route MAP-MATCHING (puck snapped to the route within 40 m), eased puck/bearing interpolation, speed-adaptive zoom, a look-ahead camera, the traffic-colored route line, a speed badge + offline speed-limit disc, and auto-recenter after a pan. Assistant geo: intents route into it (with a fix for opaque-URI parsing that crashed the old handler). The phone stays the nav brain: the same NavSession singleton drives both, so a trip started in the car shows on the phone and vice versa. Day/night keys off the host's own dark-mode signal.
  • โœ… Turn-by-turn engine (step advancement, off-route detection, reroute) - pure/Android-free, unit-tested (arrival-requires-final-maneuver, reroute fires once per off-route transition not per fix, off-route clears on return). Maneuver-sync fixes (2026-06-27, from a real highway drive where the line was right but the spoken/announced turns were ~6 mi out): (1) the prompts + step advancement now measure distance to the maneuver ALONG the route, not crow-flies - crow-flies fired "take the exit" miles early wherever the highway curved back near the exit, then skipped the real one; (2) placeManeuvers now pins each turn to the START of its step, not the end (it added the step length before placing, so every turn sat a whole step too far - invisible on short city steps, miles off on a long highway step). The blue route line was always correct; these were both in the maneuver layer on top of it. Regression-tested so they can't come back: a curved route where crow-flies โ‰  along-route (must not skip a loop-back turn that's meters away as the crow flies but km along the road), a long highway step (the turn lands at the step's start), and imperial vs metric phrasing
  • โœ… Offline nav auditor - the cards/voice-vs-blue-line diff (2026-06-27, from the same highway drive, so this class of bug can be seen from a log instead of recalled). The navigated route is now saved into the recorded trip (RP/RD/M lines via core/replay/TripLog, one format shared by the writer in :app and the reader in :core), so a replay drives the exact blue line the user saw, not a fresh re-route. core/nav/NavReplay then replays a trip's GPS fixes back through the real NavEngine and diffs what the banner + voice claimed against where the maneuvers truly sit on the route: per turn - announced how far out, turn-now fired?, worst card-distance error, nearest approach - and flags silent/missed turns, miles-too-early announcements, and lying card distances. One-call TripLog.audit(csv) on a shared travel log, or the on-demand harness :core:testDebugUnitTest --tests '*auditSharedTripLog' -DvelaTrip=<csv> (prints the per-maneuver report + the spoken-line-vs-route timeline). Unit-tested end-to-end: clean-drive measurements (turns announced ~400 m out, track passes through each), the flag heuristics, and a full saveโ†’parseโ†’audit CSV round-trip
  • โœ… Screen stays awake while navigating (2026-06-21) - turn-by-turn holds FLAG_KEEP_SCREEN_ON on the activity window so the next turn is always visible on a windscreen mount without tapping to wake the phone. Gated by Settings โ†’ Navigation โ†’ "Keep screen on while navigating" (default on); the flag is cleared the instant nav ends, the toggle is turned off, or the map screen leaves composition, so the display sleeps normally everywhere else (no battery drain when you're not driving)
  • โœ… Music gets a beat to pause before the first word (2026-09-21). A fresh audio-focus grant leads the first sample by 350 ms; players that pause on a duck used to be spoken over.
  • โœ… Resume after a process kill routes from a fresh fix (2026-09-21). The resumed drive waits up to 8 s for a fix newer than the launch seed, so the line no longer starts where the app died.
  • โœ… Offline routes no longer announce a turn on a road that only bends and renames (2026-09-21). Turns OsmAnd itself would skip, and lefts or rights with under 20 degrees of measured turn (a divided road rejoining its two-way continuation), are folded as renames; verified on the reported stretch against the state's own obf.
  • โœ… A paused drive draws its route in slate (2026-09-21) so the hold shows on the map, not only in the bar; traffic spans keep their colors.
  • โœ… The car-screen puck follows the puck-size setting (2026-09-21).
  • โœ… Offline search puts transit stops last unless the query asks for transit (2026-09-21): US stops are named by their corner, so a street or town word used to fill the list with them.
  • โœ… Spoken guidance via AOSP TextToSpeech (engine-selectable) - tuned for the car: a measured speech rate (0.97) + neutral pitch, and on init it auto-selects the highest-quality offline voice for the locale (engines often default to a low-quality or download-required one), so guidance sounds natural, not robotic. Speaks "Head east on โ€ฆ" (the initial cardinal is computed from the route's first leg and injected, since Google's markup only says "Head toward โ€ฆ"). Settings โ†’ Voice lists installed engines, a Test voice button (hear it on your hardware), and a System voice settings shortcut to install/download a voice. Spoken distances now follow the Units setting (2026-06-27): the prompt distance was hard-coded "In N meters" regardless of Imperial/Metric - now NavEngine phrases it per the preference ("In 500 feet" / "In 0.2 miles" vs "In 150 meters"), with the imperial flag threaded from Units.imperial through NavSession.onLocation. Road abbreviations are expanded for speech (2026-06-27): the engine spelled out "Pkwy"/"St"/"Ave"/"N" letter-by-letter; VoiceGuide now expands them whole-word for the spoken text only ("Parkway", "Street", "Avenue", "North", "I-80" โ†’ "Interstate 80"), while the on-screen banner keeps Google's compact forms
  • โœ… Speaks the road name - "turn right onto Larch Way", not a bare "turn right" (a deliberate divergence: modern Google Maps shortened its spoken cue to drop the street name - a TTS-pronunciation/brevity choice, not scraper defense; the name is still in Google's data + on its banner, which is where we read it keyless). Vela generates its own TTS straight from the written instruction (which keeps "onto "), so it speaks the full phrase - the old style some drivers prefer. Regression-tested (spokenPromptNamesTheRoad). (When Google's own step text has no road name - some ramps/roundabout exits - Vela, like Google, says just "turn right"; synthesizing a name from the next road is a possible follow-up.)
  • ๐Ÿž Fixed: every turn drew a generic forward arrow (2026-06-27, on-device diagnosis). The keyless feed tags maneuvers with a GENERIC token - maneuver='TURN' / 'ON_RAMP' / 'ROUNDABOUT_ENTER_AND_EXIT' - and carries the left/right + slight/sharp in a child <turn side='LEFT' type='SLIGHT'>. mapType only knew the explicit TURN_LEFT-style tokens, so every plain turn and ramp fell through to UNKNOWN โ†’ a straight-ahead arrow in the banner + the wrong direction-coded haptic, even though the text said "Turn left onto โ€ฆ". Now the parser reads <turn side/type> โ†’ correct TURN/SLIGHT/SHARP/RAMP/ ROUNDABOUT arrows + haptics. (Road names on regular turns were always in the text and unaffected; roundabout steps carry no road name keyless - Google omits "onto X" there, a known keyless gap.) Pinned to the real captured markup (DirectionsManeuverTest).
  • ๐Ÿž Fixed: route line gradient spamming MapLibre errors (2026-06-27). routeGradient built a stops-less step(line-progress, base) whenever the line had no driven-gray and no traffic spans (any directions preview, and early nav) - which MapLibre rejects ("line-gradient Expected at least 4 arguments, but found only 2"), leaving the line unstyled and logging the error on every refresh. Now it always seeds a valid base-color stop.
  • ๐Ÿž Fixed: silent navigation - on a targetSdk-30+ build, Android package visibility hid every TTS engine (getEngines() empty, the engine couldn't be bound) so guidance was silently dropped. A <queries> for TTS_SERVICE restores it; picking an engine now actually re-inits TTS (it used to be ignored). (verified on-device: audio focus + frames delivered on nav start)
  • โœ… One-tap voice on a ROM with no TTS - many degoogled ROMs ship no engine at all, so Settings โ†’ Voice โ†’ Install eSpeak NG / Install RHVoice downloads the latest open-source engine from F-Droid (resolved via its API) and hands it to the system installer; once installed it's a normal engine Vela already drives - no heavy synth bundled into the app, works on any ROM. A nav-start hint points there if you have none. Pipeline polished 2026-06-19: the install button now shows an inline spinner while downloading (not just a persistent map banner), the status auto-dismisses, and when the direct APK URL 404s - eSpeak ships per-ABI split APKs, so the single-file path failed silently - it now falls back to opening the F-Droid page so the install still completes. (verified end-to-end on-device: download โ†’ install โ†’ eSpeak appears as a Vela engine โ†’ speaks)
  • โœ… Mute voice during nav - a speaker toggle in the nav bottom bar silences / restores spoken guidance on the fly (Google-style), independent of the haptic cues
  • โœ… Speedometer - a Google-style circular badge (bottom-left during nav) shows your current GPS speed in mph or km/h (follows the Units setting). Settings > Navigation "Show speed and speed limit" hides it and the limit sign, on the phone and on the car screen (2026-09-29, #625).
  • โœ… Scale bar - a Google-style โŠ” bracket (bottom-left, by the attribution) sized to a round distance, with the distance label above it; reads the live meters-per-pixel from the map (correct for zoom and latitude on Mercator) and follows the Units metric/imperial preference (m/km โ†” ft/mi). Updates as you zoom/pan
  • โœ… Pan-away + Re-center - dragging the map during navigation detaches the follow-camera so you can look around (it stops snapping back on every GPS fix); a Re-center button appears and reattaches it, then hides once you're following again (Google-style)
  • โœ… Haptic turn cues - a light "get ready" tick at the pre-turn prompt, then a firm direction-coded buzz at the turn (left = two long pulses, right = three short, straight/other = one), so you can navigate by feel while biking/walking. Toggle in Settings โ†’ Navigation ("Vibrate on turns", default on)
  • โœ… Google-style maneuver banner - a large directional turn arrow (the maneuver-type glyph, not a generic icon), the distance, the instruction with inline highway/exit shields, a lane-guidance strip, and a compact "then โ€ฆ" preview of the maneuver after this one - + remaining time/distance and the arrival clock time ("3.4 mi ยท 7:42 PM") on the bottom bar. The Steps control is icon-only + the ETA column flexes so a long "X mi ยท 7:42 PM" never pushes the End button off-screen
  • โœ… Swipe the banner to look ahead - drag the maneuver banner left/right to walk the upcoming steps (Google-style): the card tracks your finger and, past a threshold, slides off and the next/previous step slides in (a pager-style flick, not an instant swap); it grays out, shows that step, and the map's marker + camera move there; tap it to resume live guidance. The re-center button also appears while previewing a step and snaps you back to the current step (not just the camera) - previously it left you parked on a previewed turn
  • โœ… Traffic-colored nav ETA - the big remaining-time readout in the nav bar is tinted by live traffic (green free-flowing โ†’ amber โ†’ red), or the normal ink color when there's no live data (offline / a traffic-less route)
  • โœ… Minimisable route chooser - a drag handle on the directions panel: swipe it down to minimize (peek the whole route on the map before you commit), swipe up or tap to bring it back; a compact Start stays reachable while minimized
  • โœ… Directions step list / overview (before and during nav); tap a step to preview that turn on the map - placed at its true cumulative distance along the route line (matching the polyline's own length, not the summed step distances), so the previewed spot lands on the actual turn. Pre-nav steps + ETA now match nav from the start (2026-07-02): the default route can be a provisional Google alternate (it sorts to the top on live ETA) whose turn-by-turn is a placeholder and whose ETA is over un-snapped geometry - so the preview showed wrong turns/ETA that only "corrected" when you hit Start. route() now eagerly names a provisional default (OSRM-snap + re-applied traffic) the moment it's built, exactly as picking an alternate does, so preview == nav. Step maneuver glyphs are bigger (2026-07-02): they were Modifier.size(24.dp).padding(end=16.dp) - the padding insets the glyph inside the 24 dp box, shrinking it to ~8 dp; now a real 30 dp icon + a separate spacer. Lane arrows highlight only the turn you're taking (2026-07-02): in a valid lane, only the arrow whose direction matches the maneuver lights bright (the other directions the lane also allows stay dim) - so a "go straight" step no longer lights the right-turn head on a straight-or-right lane; when the maneuver side isn't encoded (merge / roundabout) a valid lane lights all its arrows rather than guessing; compound two-head arrows widened so they don't overlap.
  • ๐ŸŸก Foreground navigation service - guidance continues with the app backgrounded / screen off via an ongoing notification (NavigationService, a location-typed FGS; POST_NOTIFICATIONS requested on Start). Google-style content: the next turn led by distance ("In 500 ft ยท Turn right onto Main St"), ETA ยท distance remaining, "faster route available" when one is, a dedicated nav status-bar icon (ic_nav, not the launcher logo), an End action, and tap-to-reopen. Updates live off NavSession; best-effort (a blocked FGS start on Android 14/GrapheneOS falls back to in-app nav, no crash). Open quirk: Start can drop the activity to the launcher while the service keeps running - needs on-device repro (ROM/timing-specific)
  • โœ… Periodic live re-routing - every ~2 min while underway Vela re-checks traffic and, when a meaningfully faster route exists, announces it and offers a one-tap switch (NavSession.maybeRecheck, RECHECK_INTERVAL_MS = 2 min)
  • โœ… Free-drive follow glides like navigation (2026-07-14). The follow camera used to ease toward the raw once-a-second fix - a target that jumps and then sits still, so the camera surged and stalled every second (the reported jitter and snap-back). It now dead-reckons the target forward between fixes along the last fix's own speed and course, exactly the trick the nav puck uses, so the camera chases something that moves like the car: a continuous inertial glide, no route required. Bounded to 2.5 s of blind projection and only while genuinely moving; each new fix re-anchors and the ease soaks up the correction instead of snapping.
  • โœ… Free-drive follow drives HEADING-UP, like navigation (2026-07-15; superseded the 2026-07-14 north-up pass for driving). Driving with no route now rotates the map so the puck always moves up the screen, never sideways - once you're at driving speed the camera eases to the GPS course, tilts to navigation's 55 degrees, and aims a speed-scaled distance ahead of the puck so the road ahead owns the view (the same framing nav gets from camera padding, done here with a projected aim point so nothing sticky survives the follow). Stopping at a light holds the heading; only ending the follow releases driving mode. The puck's own heading prefers GPS course over the compass while driving (car bodies wreck magnetometers), so the arrow and the map always agree. Walking or slow browsing keeps the north-up flat behavior from 2026-07-14 (leftover rotations still self-correct, the compass shows while settling), and a manual rotate is a gesture, which drops follow - nothing fights the user's hand. Pinch zoom is preserved in both regimes.
  • โœ… Route overview button during navigation (2026-07-14). A new fly-over button in the nav button stack fits the whole route on screen - camera only, Google-style: guidance and voice keep running and the puck keeps moving along the overview. It marks the camera detached, so the existing Re-center button appears and glides straight back into the follow. (Google hides the step list behind a pull-up on its ETA bar; Vela keeps the dedicated Steps button instead - the bar has three controls and room for them, and the overview button is the part that earns its place.)
  • โœ… Dense-area cold load: steadier frames, no icon shuffling (2026-07-14). Two costs fixed in the streamed first paint of a POI-heavy area: the collision priority used to be the place's position in the list, which RE-RANKS as search terms land, so every partial repaint reshuffled the whole layer's placement and icons visibly consolidated and popped into each other - the priority is now the place's own prominence, identical in every upload, so placement holds still while the set grows. And the partial repaints themselves escalate their batch size (10 places for the fast first dots, 25 once the map is populated), so a dense downtown runs roughly half the full-layer placement passes during the load. Off-route counting is gated off while stationary so red-light multipath can't reroute a parked car, but creeping out of a parking lot sits under that 2 m/s floor the whole way - the deviation never accumulated hits, the reroute never fired, and the blue line stayed stale. A FAR deviation (90 m+, comfortably beyond anything stationary jitter invents) now counts at any speed.
  • โœ… Perf audit remediation, batch 2 of 2 (2026-07-15, PR-gated for a canary drive). The upload-churn surgeries from the July 2026 performance audit. (1) applyData's whole tail is now identity-gated like its head: alternates (up to 4 full polylines re-tessellated per recomposition with the chooser open - during the exact fit flight being watched), the location dot, the step-preview pin, the browse route gradient, the me-layer property sets and the ensure* style probes all skip when nothing changed, with every new gate registered in the style-reload reset so theme/palette/satellite flips still repopulate. Route-mode visibility flips became transition one-shots. (2) Ambient POI uploads DEFER while a discrete camera flight is in the air (a depth counter around the locate/fit/overview/preview flights - never the per-frame follow tickers, which would starve paints for whole drives); the first recomposition after landing uploads the full set at camera-idle where the placement pass is invisible. (3) The in-nav route split re-uploads only a 3 km leading WINDOW every 150 ms; the far tail lives on a twin layer refreshed once per window advance, with traffic spans remapped onto each piece and the seam computed identically on both sides - it also sharpens the traffic gradient (256 texels over 3 km instead of the whole trip). (4) The walk/bike dot regen went from millions of iterations per pinch (a fresh whole-polyline scan per dot) to one monotonic pass. (5) The speed-limit poll skips while a flight or two-finger gesture is in progress. (6) Result-pin bitmaps survive style reloads in a small process LRU instead of re-rasterizing on every theme flip.
  • โœ… Perf audit remediation, batch 1 of 2 (2026-07-15). A 43-agent audit (every finding adversarially verified) confirmed 21 issues; the six camera-seam fixes landed first because they ARE the reported hitches. (1) Re-attaching the nav follow from the route overview eased position/zoom/bearing but snapped tilt and the puck-low padding whole on frame one - both now seed from the live camera and ease in, so Overview -> Re-center glides. (2) Detaching the nav camera (a pan, the Overview tap) let the camera logic fall through to the pre-nav route-fit branch, which fired an uninvited 800 ms whole-route flight on the next ~1 Hz recomposition and raced the real overview fit - the branch now swallows during nav. (3) The follow's cold engage from a far-out camera was an instant teleport that also canceled the launch flight; it is now a single owned flight the follow waits out. (4) The compass pushed up to 16 whole-screen recompositions per second while hand-held; a 200 ms floor caps it. (5) Ending or swapping a route in the CHOOSER ran the nav camera teardown and killed the route-fit flight at frame one - the teardown now fires only after a real drive. (6) All point GeoJSON sources cap their internal tile pyramid (maxZoom 12), cutting re-tiling and placement invalidation during every zoom change. Batch 2 (upload gating, deferred ambient paints during flights, the route-split window, walk/bike dot regen) is queued in the audit doc. The visible signal/stop icons sat at the very bottom of the symbol stack - under the bridges and both blue route lines - so the (now wider) route stripe painted straight over exactly the icons that matter most during a drive. They now draw above the route lines and bridge geometry but below the basemap text and every POI layer, the way Google paints its signals on the route. Street names still can't collide with them: the invisible claim twin keeps placing an always-on collision box at every sign, so labels dodge sign positions regardless of where the visible icons paint.
  • โœ… In-nav compass toggle: heading-up or north-up (2026-07-15). Tapping the compass that already sits below the maneuver card flips the follow camera between the default heading-up view and a north-up flat view that still follows the puck (position, speed zoom, puck-low framing all kept; the arrow rotates on the map instead of the map rotating under it), easing over a beat rather than snapping. Outside navigation the compass keeps its stock reorient-to-north tap. (First cut added a separate button to the nav stack; replaced same day with the existing compass per feedback.)
  • โœ… Two-finger tilt works during navigation (2026-07-15). The shove gesture had no guard, so the follow ticker rewrote its own tilt every frame and the gesture jittered and lost. The ticker now steps aside during a shove (like a pinch), the resulting tilt sticks as a per-drive override the way pinch zoom does, and a shove's incidental finger drift no longer reads as a pan that detaches the camera. Cleared when the drive ends.
  • โœ… Route overview fits the WHOLE route (2026-07-15). The in-nav overview used to fit the bounds while keeping the follow camera's rotation and 55-degree pitch, which shows less than the whole route however correct the math. It now levels to north-up flat as part of the fit, like Google's overview.
  • โœ… Locate button restores a consistent zoom (2026-07-15). The tap's zoom animation was being canceled mid-flight by the free-drive follow's per-frame camera writes, so the final level was wherever the animation happened to be when interrupted. While the follow is active the standard zoom now rides the follow ticker itself (position and zoom ease together, uncancellable); the plain animation stays for the not-following case.
  • โœ… Places-on-the-map controls (2026-07-15, Settings). A new section with a master "Show places" switch (off = a clean basemap - the ambient layer, its fetch AND the OSM fallback business POIs all hide; searched results still draw), a "Parks, schools and civic places" toggle (drops the non-business tier from the ambient pool, filtered in state so tap indices stay aligned), a "Transit stops" toggle (icons + their per-viewport fetch), and a "Place icon size" choice (Small / Default / Large) that scales the ambient icons+labels, the mini-dot tier, the OSM POI icons, transit stops and traffic controls together - Small exists for low-density car head units (a 1024x600 screen renders the fixed-px bitmaps physically huge); phones default to 1.0 and are untouched. All toggles act immediately. Since 2026-07-20 the scale also covers the layers the first cut missed: search-result pins and rating bubbles (with their labels), the collapsed result dots, saved-place pins and the surveillance-camera badges - so a gas or restaurant search on a head unit shrinks with the rest of the map. Transit-stop badges also claim a collision box now, so nearby labels place around them instead of printing over them.
  • โœ… Fire stations are not bus stops anymore (2026-07-15). The category classifier's transit branch matched the bare word "station" before the civic branch could claim "fire station", so fire houses drew with the transit icon. Civic now classifies first, and the bare "station" keyword excludes the non-transit flavors (power, pumping, radio, ranger, weigh, polling stations and friends).
  • โœ… House-number searches resolve locally, like Google (2026-07-15, reported with two local test addresses). Two holes closed. The on-device address geocoder (exact and interpolated house numbers from the downloaded pack) now joins ONLINE searches and suggestions for address-looking queries - it was gated to the offline/Google-failed path, so a wrong-but-nonempty Google result set blocked it completely, which is exactly what numbered streets with directional suffixes produced. And the suggest dedupe that dropped a Photon/local address hit whenever ANY Google suggestion sat within a block of it (on a commercial road, something always does) now only drops it when the Google entry carries the same house number. Address hits lead the list: local pack, then Photon, then Google.
  • โœ… Typed suggestions are Google's own autocomplete (2026-09-22). The keyless search-as-you-type request the maps web page fires, with the viewport as the bias, so a partial address finally ranks by where you are: a bare house number lists the houses with that number on the streets around you (it used to answer with a ZIP code in another state), and a full address in another city ("459 Ralston") shows the street with its city instead of nothing. Bare query rows ("Starbucks", "cvs pharmacy hours") run as a search. Pressing Enter on a typed house address the results could not place now geocodes it the same way. Offline and with Google off, the old local-pack + OpenStreetMap path is unchanged.
  • โœ… The fill-in arrow on suggestions (2026-09-22). Google's north-west arrow on every suggestion row puts the row's primary text (the name, or an address's street line) into the search box without searching, cursor at the end, so it can be refined by hand.
  • โœ… Arrival speaks ONE line (2026-07-15). "Your destination is on the right" when the route knows the side; "You have arrived" only as the fallback when it doesn't - they no longer stack.
  • โœ… Google-width route stripe (2026-07-15). The blue route line (and the gray alternates a step thinner) scales with zoom instead of a constant 6 px: browse zooms look the same, street/nav zooms draw the fat stripe Google does.
  • โœ… A degraded route can no longer masquerade as a "faster route" (2026-07-15, real-drive report). When the ~2-min recheck's Google fetch fails, the candidate comes back trafficless with abbreviated steps - and its free-flow ETA, compared against the live traffic-aware remaining time, always looks like a big saving. Accepting one explained a whole cluster of symptoms in one drive: a suspiciously fast ETA, white ETA text instead of traffic colors, no lane guidance (abbreviated Google steps carry no lane data), and the time "syncing back" to Google's a recheck later. Three gates now: a trafficless or abbreviated candidate is never OFFERED as faster (its ETA isn't comparable), a trafficless candidate never drives the live ETA recalibration, and the same-course self-heal that already restored full steps after an OSRM blip now also restores live traffic - a white ETA turns traffic-colored again on the next recheck instead of staying white for the rest of the drive (neither quality can downgrade in the swap).
  • โœ… Faster wrong-turn rerouting (2026-07-15, reported: "waits far too long"). Three coordinated cuts to the off-route detection lag in NavEngine: the on-route corridor tightened 45 โ†’ 40 m (moving GPS error is well under 20 m plus a lane offset, and on a shallow-angle wrong road the corridor width is most of the wait), the jitter debounce dropped 4 โ†’ 3 consecutive off fixes, and a fix that is BOTH moving and unambiguously far off (90 m+, beyond anything jitter invents at speed) now counts double - so a clearly wrong road triggers the reroute after ~2 fixes instead of a full debounce. Stationary protection is untouched: a parked car at a red light still never reroutes.
  • โœ… Swipe down to dismiss the steps overview (2026-07-15). The turn-by-turn step list (route preview and in-nav) now rides the finger down and dismisses on a flick or past a third of its height, springing back otherwise - the same drag grammar as every other sheet, with a grab handle to signal it. The X button and back gesture still work; drags over the list itself keep scrolling the list.
  • โœ… Google's label density at browse zooms (2026-07-14). The ambient POI layer used to name every dot its collision pass could fit; Google names only a handful of anchors at browse zoom and lets the rest sit as bare icons/dots, with more names joining as you close in (A/B'd against the Google Maps app on the same downtown frame: ~7 named at their z15, ~13 plus bare dots at z17). Labels are now tiered by zoom x prominence: below z15.5 only true landmarks (prominence โ‰ฅ 6.0, roughly 400+ reviews) carry names, z15.5+ adds established places (โ‰ฅ 5.0), z16.5+ names any real business (โ‰ฅ 3.0, ~20+ reviews) while 0-review junk stays a dot, and z17.5+ names everything visible. Doubles as a dense-area frame win: an empty textField skips that symbol's label placement entirely, so browse zooms in a POI-heavy downtown run far fewer collision candidates per placement pass.
  • โœ… Fixed: ambient prominence silently zeroed on cold starts (2026-07-14). Live-bisected on device: for the first ~3 seconds of a fresh session Google serves a stripped per-place block - rating present, review count ABSENT - and the exact same query+pb returns the full block once the session warms. The ambient category fan-out fires immediately at cold start, so its whole pool parsed with no review counts, which zeroed ambientProminence and quietly flattened everything keyed on it: prominence ranking (the anchor-beats-tenant logic), data-driven dot sizing, and the new label tiers all ran on zeros (and the disk cache then preserved the zeroed pool). nearbyPlaces now detects the slim flavor (rated places but a majority missing counts) and refetches once after the session warms; the rich pool replaces the slim one and the cache stores real counts. Costs one extra fan-out on cold start only.
  • โœ… Tunnels: navigation keeps estimating (2026-07-14). When GPS dies mid-drive while solidly on route, Vela now synthesizes position ALONG THE ROUTE at the last speed (decaying over ~a minute, capped at 3 km of blind travel) and feeds it through the normal guidance path - so the arrow keeps moving, the banner keeps counting down, and turns still announce inside the tunnel, Google-style. The "Searching for GPS" chip stays up the whole time so the estimate is honest, and the first real fix re-anchors everything. Never during replays, never off-route, never from a standstill.
  • โœ… Nav declutter: no bus stops (2026-07-14). The canonical transit-stop icons and their name labels hide during turn-by-turn (browse furniture, not a driving aid) and the per-viewport stop fetch skips while navigating.
  • โœ… Closer nav zoom (2026-07-14, closer again + 30% bigger puck 2026-07-15). The speed-adaptive follow zoom now runs 18.5 (stopped) down to 15.8 (highway) - originally 17.3-15.0, then 18.0-15.5 - with the puck riding low, the extra zoom is all road ahead. Pinch still overrides. The nav puck grew ~30% alongside it (176 px disc, redrawn at native size, not raster-scaled).
  • โœ… The road ahead owns the nav view (2026-07-14). The follow camera renders the puck low on the screen (~72% down, via a sticky top camera padding reset when nav ends) instead of dead center, so the view shows what's coming instead of splitting evenly with what's behind - Google's framing. Applies to the engaged follow and the pre-engage stationary framing alike.
  • โœ… Step preview no longer fights the follow camera (2026-07-14). Swiping the maneuver cards flies the camera to the previewed turn, but the 60 fps follow ticker kept re-pointing it at the puck in the same frames - the two fought and the preview barely moved. Follow now yields while a preview is up (the puck keeps updating the whole time, only the camera steps aside), and Re-center or ending the preview hands the camera back with the usual smooth re-attach.
  • โœ… "Live traffic re-checks" privacy toggle (2026-07-14, Settings โ†’ Data & privacy). The ~2-minute in-drive re-check sends your CURRENT position to Google - that's what makes a from-here candidate possible, and it's a periodic location beacon on top of the origin sent at start and on reroutes. It's now opt-out-able: off means no periodic requests (and therefore no faster-route offers, live ETA recalibration or abbreviated-steps self-heal); off-course reroutes still work, because navigation can't without them. PRIVACY.md documents the whole in-drive picture.
  • โœ… The traffic fetch retries like the router does (2026-07-14, from a real-drive report). Google's keyless directions reply intermittently comes back degraded or empty (worst right after a burst of requests, like ending a route and restarting it), and it used to get exactly ONE attempt while OSRM got three - a single miss cost the whole fetch its traffic ratio, its jam-avoiding snap AND Google's alternates, so the picker led with free-flow OSRM routes whose white, trafficless ETAs read minutes faster than anything traffic-aware and varied drastically between restarts. googleDirectionsRetried now gives it the same 3-attempt backoff; a genuinely unreachable Google still degrades to free-flow, just honestly rarer.
  • โœ… Abbreviated-steps self-heal mid-drive (2026-07-14, from the same report). If the open router is unreachable when a reroute fires, the fallback is Google's keyless route: complete polyline, ABBREVIATED maneuvers (a 6-mile route once carried 2 of ~10 turns) - the banner and voice then disagree with the blue line, and the line is the one telling the truth. Those routes are now tagged at the source (Route.abbreviatedSteps, also set when naming a picked alternate fails), and the ~2-minute recheck upgrades an adopted one SILENTLY the moment a full-stepped same-course candidate comes back: same path, fresh traffic, real turns, instead of staying degraded for the rest of the drive.
  • โœ… The arrival ETA tracks LIVE traffic even with no course change (2026-07-14). The engine's remaining time scales the leftover step durations by the route's traffic ratio, which used to be frozen at whatever the traffic looked like at the LAST route fetch (nav start, or a reroute) for the entire drive - a jam forming ahead never moved the arrival time until you deviated. Now the same ~2-minute recheck that hunts for faster routes also RECALIBRATES the shown ETA: when the fetched candidate follows the route you're already driving (all sampled points within 250 m of the current line - SAME_COURSE_M, deliberately tighter than the 700 m jam-detour test so a parallel alternate can't calibrate the wrong road), its fresh traffic-aware ETA becomes the new baseline (etaScale, a multiplicative correction on every published remaining duration - the phone banner, the notification and the Android Auto card all read it). The correction resets whenever the route itself is swapped (a fresh route carries fresh traffic), is clamped to 0.5-2.5x against a bad candidate, and the faster-route offer logic now compares against this live baseline too. Hermetic replays never recalibrate (no live fetches, as before).
  • ๐ŸŸก Posted speed-limit badge (2026-07-04; LIVE everywhere online via the streamed overlay below - only the offline-graph path still needs re-baked graphs). During nav a Google-style speed-limit sign shows by the speedometer - US MUTCD style (white rounded rect, "SPEED LIMIT" + number) in imperial, EU/RoW (white disc + red ring) in metric, and the number reddens when you exceed the limit + a tolerance (GPS speed is noisy). Source is OSM maxspeed, keyless + offline
  • not Google (Google gates posted limits behind the paid Roads API; they're absent from the keyless payloads). Read from the on-device region file Vela already ships: currentRoadLimit(lat,lng) snaps the live fix to the nearest way of the downloaded obf (OsmAnd's findRouteSegment, 25 m) and reads the way's maxspeed - route-independent (tracks the road under the puck even off-route), off the main thread, distance-gated + single-flighted per fix (MapViewModel.updateSpeedLimit); a sustained untagged stretch clears a stale limit rather than showing it forever. Crash-safe by construction (every engine call is runCatching-wrapped): no file, no badge - no crash, no routing regression. US mph round-trips exactly; 150 km/h and a derestricted maxspeed=none are deliberately blanked (a wrong number on an autobahn is worse than none). Until 2026-09-15 this read the GraphHopper graph's max_speed encoded value. Coverage = OSM maxspeed (partial) - strong on highways/EU/urban, sparse on US residential. Remaining to light it up for everyone: re-bake + re-host the region graphs with max_speed (CI), then a fresh region download shows it (a version-discriminator so existing offline graphs auto-re-download is a small follow-up). Now shown in FREE-DRIVE too (2026-07-12, user request): the same updateSpeedLimit already ran on every live browse fix, it was only rendered during nav; the sign now also appears while you're simply driving with no route open (moving, on the clean map); it shares the unified speed box, which in free drive sits LOW in the corner, level with the locate button (2026-07-14) - only during nav does the box ride up to clear the ETA bar, and the scale bar yields the corner whenever the box is there. Speed A of the plan; Speed B (a keyless maxspeed PMTiles overlay) will remove the download-a-graph requirement so limits show anywhere, and the same badge lookup will fall back to it when no graph covers the road. shows it** (a version-discriminator so existing offline graphs auto-re-download is a small follow-up).
  • โœ… Speed-limit STREAMER - online maxspeed everywhere (2026-07-12, device-verified on a simulated drive). The "Speed B" source so a posted limit shows without a downloaded routing graph. The data half already ships: scripts/build-maxspeed-region.sh + CI bake each region's OSM maxspeed ways into a small PMTiles overlay on the maxspeed-overlays release (all 50 US states hosted, a large state is 31 MB). The app half now streams the covering region's PMTiles (MaxspeedOverlayStore, no download - MapLibre range-fetches the visible tiles), adds it as an invisible-but-queryable line layer, and every ~2.5 s while driving queryRenderedFeatures under the puck reads the maxspeed tag (OsmMaxspeed.parseKmh, unit-tested: bare km/h, mph, none/country-codes โ†’ null). The sign now prefers the offline graph and falls back to this overlay (speedLimitKmh ?: speedLimitOverlayKmh), so it lights up anywhere online. Keyless, ODbL. Device-verified on a simulate-driving run (4a, no routing graph downloaded): the streamed overlay read 72.42 km/h under the puck on the arterial and the sign showed "SPEED LIMIT 45" (offline=null). Parser unit-tested; the state's maxspeed PMTiles streamed live (~640 range requests) and kept up with the demo puck.
  • โœ… Speedometer = a rounded rectangle now, not a circle (2026-07-12). The traveling-speed readout during nav was a dark circle, which read differently from the rounded-rect SPEED LIMIT sign right above it. It's now a rounded rectangle the same width + corner radius as the limit sign, so the two stack as a matched pair (Google's layout) and your speed is trivially comparable to the posted limit at a glance. Dark fill keeps it distinct from the white limit sign. Device-verified paired on a drive: "SPEED LIMIT 45" over "45 mph".
  • โฌœ Speed-camera + hazard alerts (lane guidance โœ… done above)
  • โœ… Android Auto - first cut shipped 2026-07-08 (see the entry at the top of this section). The old "needs GMS, out of scope" call was wrong: the car app library is plain androidx, and a sideload runs fine with AA's Unknown sources on. Remaining: car-side search + route start, and a real head-unit drive.
  • โœ… Arrival / trip summary - on reaching the destination, a "You've arrived" card replaces the nav controls with the trip's total time and distance (and the destination name), and a Done button returns to a clean map. Fixed 2026-06-20 (test-drive bug): nav was declaring "arrived" up to tens of km early - Google's step distances total a few % short of the route geometry, so placeManeuvers placed the ARRIVE maneuver at sum(stepMeters)/polyLength < 1.0 (observed ~15 km short of a 134 km route's end) and the 25 m arrival trigger fired there. The final maneuver is now pinned to the route end; diagnosed + verified on-device by replaying the recorded trip (arriveLoc now equals the destination). Unit-tested. (Foreground-service + live re-route hardening still pending a full on-device drive.)

Location (degoogled)

  • โœ… AOSP LocationManager (GPS + NETWORK), no Fused/GMS
  • โœ… Last-known seeding for instant map; PSDS slow-fix tip
  • โœ… Google-style location indicator - two modes. Browse: a blue dot (Google's #4285F4) with a white ring + a translucent heading cone/beam beneath the dot - points the device-facing compass (the TYPE_ROTATION_VECTOR sensor via HeadingProvider, low-pass smoothed along the shortest arc, OsmAnd-style) so it shows which way you're facing even standing still, where the GPS bearing is noise (2026-06-27); falls back to GPS course when the sensor's absent, hidden when neither is available. (Navigation never uses it - there the heading comes from the matched road; the sensor heading is pushed to state only in browse and only on a real change, so it can't spam recomposition during nav.) The dot grays out when the fix is stale (~12 s, or before the first live fix) and blue again on a fresh fix. Free-drive follow (2026-07-11, user request - "driving along without a route open, the camera should track me, and the heading beam should be smooth like in-nav"). When the map is open and unobstructed the camera now tracks your fix north-up and the heading beam eases per frame the same way the nav puck does - a sibling of the nav motion ticker (LaunchedEffect(navMode, driveFollowing) in VelaMapView) that OWNS the location source while it runs, so the beam glides instead of chasing every jittery compass sample (the raw per-recomposition repaint is what made it twitch). On by default; a pan drops follow (Google-style "let me look around") and the locate FAB re-arms it. Suppressed whenever a focus surface owns the camera (search, a selected place, directions, the results list) so it never fights that framing, and idle frames are skipped (a settled follow at a red light doesn't re-upload the point or re-drive the camera 60x/s). Nav keeps its own tilted, speed-zoomed follow. (Feel tuning - the 0.16 s camera ease + 0.15 s beam tau - to confirm on a real drive; revertible.) Nav: a solid blue arrow puck (the dot is hidden) that snaps onto the route line and faces down the road, so lateral GPS jitter doesn't make the marker jump. The snap is honest, not a lie: it engages only when you're genuinely following the road - within ~22 m (โ‰ˆ a road width + GPS error) and heading the road's way (ยฑ55ยฐ); a missed exit / off-road / wrong-way fails the gate and shows the real position so you can see the divergence (then the off-route reroute kicks in), Google-style (arrow puck + heading-gated snap added 2026-06-20 from test-drive feedback - "we need an arrow", "don't snap so it's out of touch with reality"; verified on-device via trip replay). The puck rides a Google-style forward-progress motion model (added 2026-06-21) instead of teleporting to each raw fix: a per-frame ticker glides it monotonically forward along the route with dead reckoning (between the ~1 Hz fixes the position keeps advancing, so it never stalls mid-second), eased progress (it never jitters backward) and smoothed heading (rotates through bends instead of snapping at every vertex). Accelerometer-fused speed (2026-07-01) - the dead reckoning no longer glides at the last fix's speed (which overshot the moment you braked, and the monotonic progress could never walk it back - the puck sat meters ahead of a stopping car until it "caught up"): a 1-D Kalman filter (core/location/SpeedKalman, unit-tested) fuses the GPS speed (measurement, each fix) with the measured forward acceleration (prediction, every frame - TYPE_LINEAR_ACCELERATION rotated into the world frame by MotionProvider and projected onto the travel bearing), and the reckoned advance is the integral of the modeled speed (โˆซvยทdt, still blind-capped at 2 s). So braking collapses the predicted speed NOW, Google-style, and the puck decelerates with the car between fixes. No accelerometer โ†’ a 0 โ†’ exactly the old constant-speed behavior (and the fiter still jitter-damps the GPS speed). The speed-adaptive zoom rides the same filtered speed. Road-test pending - tuned constants in SpeedKalman.)* The oroute match is forward-only and monotonic, modeled on OsmAnd's klzzh:2335 (2026-06-21): oncengaged it searches a bounded look-ahead AHEAD of the current progress only** (klzzh:2336, from targetM โˆ’ 25 m to targetM + speedยท8 s clamped 150โ€“600 m) - never behind, never the whole route - so a route that passes near itself (switchback / cloverleaf / parallel return leg, or an ordinary road that runs close to itself) physically can't pull the puck onto a far or earlier leg. When nothing's ahead within tolerance (a GPS spike or a real off-route) it HOLDS and dead-reckons forward instead of re-snapping globally - the old global fall-back is exactly what teleported the camera to "a random spot along the route" - and only a sustained run of misses (~6 fixes) disengages to re-acquire, while NavEngine's own off-route logic drives the reroute. The one global search left is the initial acquisition at nav-start / just after a reroute. (Evolution: the first 2026-06-21 pass windowed the search but still fell back to a global nearest-point on any off-route blip and could re-anchor backward - replay was "better but still jumps to a random spot"; the forward-only matcher closes those two holes at the source.) The follow-camera tracks this same smoothed point - not the raw fix - so the puck and the map move as one and the view no longer lurches to a far spot when nearest-point is briefly ambiguous; off-route it now holds the last road-aligned heading instead of spinning to the raw GPS bearing (which jittered and could point the map the wrong way on a brief off-route blip). The traversed-gray split rides the puck's DRAWN position (progressM) - exactly where the arrow renders, not the targetM it's easing toward - and is drawn as a hard step at the EXACT fraction (2026-06-21), not quantized to a 256-sample grid: the old sampling left the gray/color boundary up to route-length/256 m (~80 m on a long route) ahead of the arrow, which read as a soft "gradient" before driven areas; now it's a clean solid cut dead under the puck, no feathering ("we either drove it or we didn't"). The cut is also refreshed in the 60 fps motion ticker (2026-06-22, throttled to ~3 m of progress), not just at recomposition - otherwise it lagged the per-frame arrow at speed and left a sliver of colored route just behind the arrow. (Still can't land on the wrong leg.) (Forward-only matcher unverified on a live drive at ship time - to be confirmed by replaying a recorded trip on the new build, since mock GPS is too clean to reproduce the wrong-leg snap.) Heading & speed are synthesized from movement when a GPS fix doesn't carry them (cold start, just-started-moving, some chipsets/ROMs) - gated on real movement so a standstill's jitter can't spin the marker - so the arrow always points the right way and dead-reckoning always has a speed. A single-fix speed SPIKE is rejected (2026-06-21) - a GPS glitch ("going 35, hops to 157 mph"); a car can't gain >15 m/s between fixes, so the prior speed is kept, in both the live and the replay paths (recorded traces carry the raw glitches too), so the speedo, dead-reckon and zoom don't lurch on a bad reading. Position OUTLIERS are rejected the same way (sanePosition, 2026-06-21): a coarse NETWORK / multipath fix that leaps farther than is physically plausible for the elapsed time - the network provider interleaving with GPS is the "every ~8 s the dot + distance + mph jump to a crazy number" jitter - is dropped, and the position runs through a speed-adaptive low-pass (2026-06-22) - heavy smoothing at low speed so parked/idle GPS jitter barely nudges the dot, easing to a 1:1 follow by ~10 m/s where real movement dominates the noise. Google smooths position this way; OsmAnd notably does NOT (its dot hops - it only Kalman-smooths the heading from sensors), so this is actually steadier than OsmAnd at idle. (Replaced a binary standstill-hold whose hard speed-cliff the GPS speed-noise kept tripping.) Live + replay. (2026-06-22 fix: the first fix of a session anchors and a leap that persists โ‰ฅ2 fixes re-anchors - without it, a replay that starts away from your live position rejected every fix as an outlier vs the stale start point and the dot never moved ("replay thinks I'm stationary").) (motion model + camera-locked-to-puck + derived heading/speed; verified on-device 2026-06-21 via a mock-GPS drive of a real 14.5-mi Davisโ†’Sacramento route: arrow puck tracking, solid gray-behind/blue-ahead route, heading-up camera locked to the puck through the I-80/US-50 interchange with no snap, and the banner/HUD counting down monotonically - remaining 14.5โ†’11.9 mi with 0 monotonicity violations across 75 nav updates)
  • ๐ŸŸก BeaconDB WiFi positioning - NETWORK-provider coarse fixes are already used for the browse dot when GPS has been quiet (never during nav); an explicit opt-in and any deeper use are still open

Offline

  • โœ… Offline search finds the shop at a typed address and shows addresses on results (2026-09-21). An address query leads with the businesses standing at that address (from the place pack, 40 m), then the house point; the first 20 results with no address of their own are filled from the address index, the way the sheet already did on select.
  • โœ… No more style reloads while panning along a download's border (2026-09-21, issue #552). An offline basemap is mounted only once the whole visible view sits inside its data, and unmounted as soon as the center leaves it, so the view near a border streams instead of flipping.
  • โœ… A downloaded region can no longer blank the map (2026-09-18, issue #552). Two things were wrong at once. The bake let planetiler inherit the extract's header bounding box, which for 37 of 413 regions claims far more ground than the archive holds (one reached seven degrees into the next state), so the app mounted an archive for views it had no tiles for and the vector map drew nothing while traffic colors and place pins still painted on bare land. And the pick itself was "the smallest box covering you", which is unsound even with honest boxes: a box is a rectangle and a state is not, so a neighbor's box routinely covers ground its data never reaches. The bake now pins its bounds to the region's own polygon, and the app asks the archive whether it actually draws roads where you are before mounting it, falling back to the old pick when a file cannot answer. Verified against the reporter's own screenshots: the archive that was being mounted over south-west Pennsylvania answers "no roads here", so the right one is used instead.
  • โœ… Offline maps live in Settings, not onboarding (declutter 2026-07-10; the 2026-07-07 first-run offline prompt was removed). A one-time first-run prompt used to offer offline setup right after the voice offer. It was cut to keep onboarding short - a brand-new user hasn't searched anything yet, so "download an area" has no context. Offline map + routing downloads remain fully available in Settings โ†’ Offline (the section a search-with-no-signal or the locate flow points you to), which is where the download actually belongs.
  • โœ… Settings โ†’ Offline is collapsible (2026-07-02) - collapsed by default (tap the header to expand), so its long routing-region list doesn't force a big scroll past to reach the sections below it.
  • โœ… Offline basemap region downloads - Settings โ†’ Offline โ†’ Map area โ†’ "Download the area you're viewing" saves the last on-screen area's tiles/glyphs/sprites (via MapLibre's built-in offline store) so it renders later with no network; the same section manages/deletes saved areas. (Moved off the map FAB stack to declutter; the viewport is captured on camera-idle.) Open tiles, no Google, no backend.
  • โœ… Offline search - downloading a map area also pulls its POIs from OpenStreetMap/Overpass (keyless) into an on-device SQLite index; when Google search can't be reached, search falls back to that index ("Offline results"). The index now keeps the POI detail OSM carries - address, phone, website and opening hours (from the addr:* / phone / website / opening_hours tags) - so an offline place sheet isn't just a name on a pin (sparser than Google, but real). Category words work offline now (2026-07-07): the index is populated from OSM tags, so a gas station is stored as category "Fuel" (from amenity=fuel), not "gas" - a plain name/category LIKE missed it, so searching "gas" (or tapping the Gas chip) returned nothing and wrongly said "download an area". OfflinePoiStore now expands common words and the map's own category chips to the OSM tag values it stores (gas โ†’ fuel, coffee โ†’ cafe, food โ†’ restaurant/fast food, groceries โ†’ supermarket/convenience, pharmacy, hotel, bank, โ€ฆ), so offline category search works. Multi-word queries work too (2026-07-07): the search matches the whole phrase OR any word โ‰ฅ3 chars, and ranks by how many query words hit the name/category, so "fast food" leads with the fast-food places and "mexican restaurant" surfaces the Mexican one instead of returning nothing. Device-verified (airplane mode): "gas" โ†’ Safeway Fuel Station, "restaurant" โ†’ Subway/Papa Murphy's/Applebee's, "fast food" โ†’ the two fast-food spots first. And when you DO have a downloaded area but nothing matches, the message is now "no offline results for X in your saved area" instead of telling you to download one you already have. Offline search also matches a POI's stored address now, so typing the street address of an indexed OSM place finds it.
  • โœ… True offline address routing - typed address โ†’ coordinate โ†’ route, no signal (2026-07-07). Downloading a map area now also builds an on-device forward geocoder (OfflineAddressStore, SQLite), so an arbitrary typed street address resolves to a coordinate offline and routes via the on-device engine - not just addresses that happen to be an indexed POI. Two OSM sources, fetched (keyless Overpass) over a padded ~15 km box around the viewport (not just the few blocks of tiles on screen, so a saved area covers the surrounding metro): addr:housenumber points for house-precise hits, and named road centerlines (thinned to ~one point per 120 m) for a street-level fallback where OSM has the road but no house numbers - the reality in new US suburbs, where houses are sparse but streets are complete. Geocoding is layered: (1) exact house number on the street, (2) interpolate the house's position between the two nearest mapped numbers ("1022" lands between 1020 and 1024), (3) nearest mapped house on the street (right block), (4) nearest point on the street's own centerline (works with zero mapped houses). Street matching is abbreviation-normalized ("Blvd"โ†”"boulevard", "W"โ†”"west"), so "W Covell Blvd" and "West Covell Boulevard" hit the same rows. Device-verified (wifi fully off, in a residential suburb with thin OSM coverage): the download saved 8591 addresses + 1466 streets; a typed street address resolved to the right spot and routed 5 min ยท 1.5 mi on the phone; an arbitrary house number on another street resolved through the fallback layers too. Names/streets themselves are never translated (data). Routing still works to anything else you can put on the map (offline search result, long-pressed pin, Choose-on-map). Offline POIs now show an address (2026-07-07). Most US chains have no addr:* in OSM (Applebee's came back as bare state initials), so an offline place sheet showed no address. Opening an offline POI with no real street now reverse-geocodes its location against the same on-device index (OfflineAddressStore.reverseGeocode): the nearest mapped house within 60 m (usually the POI's own building), else the nearest street name within 150 m. Device-verified: Applebee's offline now shows the street it sits on plus its OSM phone/website/hours. No misleading "current traffic" offline (2026-07-07). The directions ETA subtitle only says "current traffic" when the route actually carries a live in-traffic ETA; an offline route (or any traffic-less route) shows the arrival time with no traffic note instead of a false "current traffic". Upgrade nudge for older saved areas (2026-07-07). Because the address index is built at download time, areas saved before this feature have tiles + POIs but no address data. Settings โ†’ Offline shows a one-tap "Update saved areas" card when you have saved areas but the index is empty; it re-fetches the address/street data for every saved area so you don't have to re-download each one by hand. Localized in all supported languages.
  • โœ… Whole-region offline place packs - Organic-Maps-style state-wide search (2026-07-07, device-verified). The gap this closes: Organic Maps finds a misspelled dish name anywhere in a state offline, because its download carries the whole state's POI index; Vela's offline search only knew the small viewport areas you'd saved. Now downloading a state (routing region) also pulls its PLACE PACK - a per-region SQLite database baked by CI (scripts/build-poi-region.sh + poipack_build.py, workflow poi-packs.yml, hosted on the poi-packs release like the routing graphs) from the same Geofabrik OSM extract, holding the entire region's named POIs (with address/phone/website/hours), address points and street names. The pack schema is normalized (street names deduped into an int-keyed lookup) so a state stays reasonable: a large state - one of the heaviest US states, 163k POIs + 2.8M addresses + 1.2M street points - is 143 MB zipped (was 761 MB naive). OfflinePoiStore/OfflineAddressStore query every installed pack alongside their own index (OfflinePacks), and the normalized shape keeps whole-state queries fast (street matching scans ~90k distinct names, never the 2.8M-row table; the big tables are hit through sid/housenumber/lat indexes). Packs ride the routing region: downloaded after its graph, deleted with it; regions installed before packs existed get a "Get places" button on their Settings row (and a friendly "no pack published yet" note while the catalog builds out). Device-verified end-to-end (wifi off): a misspelled dish name typed in a downloaded suburb returned the restaurant that serves it in a city across the state, with its street address (plus its two other branches), ranked by relevance then distance, and its place sheet opened with address/hours/phone/website - all OSM, no signal. Typed-address geocoding also gains the whole state (the pack's addr/street tables feed the same layered geocoder). Localized in all supported languages.
  • โœ… Place packs stay fresh: monthly rebuilds, an Update button, and small delta downloads (2026-07-07, device-verified). OSM never stops changing, so the packs can't be a one-time bake. Three pieces. First, poi-packs.yml runs on a monthly cron and rebuilds every published region from the current Geofabrik extract; each rebuild bumps that region's rev in the manifest. Second, once the manifest carries a newer rev than the one installed (tracked in poipacks/revs.json), the region's Settings row shows "Update available" and an Update places button. Third, the update is normally a row-level delta, not a re-download: CI diffs the new pack against the previous rev with SQL EXCEPT (scripts/poipack_delta.py) and publishes a small zip of del/ins tables when it comes out under half the full pack size; the app (PoiPackStore.applyDelta) applies it in a single transaction and verifies the result against per-table row counts from the manifest before committing, so a bad patch can never leave a half-updated pack. A verified whole-state test delta was 5.6 KB against the 143 MB pack. Anything off (wrong base rev, no delta published, count mismatch) quietly falls back to a full download. The piece that makes deltas small: street-name ids are stable content hashes (SHA-1 of the normalized name), not insertion counters, so a rebuild only changes the rows whose data actually changed. Device-verified end-to-end: a staged rev bump updated the state through the 5.6 KB delta alone and the changed place was searchable offline right after.
  • โœ… Offline search ranks exact name matches above the category flood (2026-07-07). A whole-state pack has thousands of rows matching a category word ("cafe"), and the internal 400-row candidate cap used to fill in table order before a name match deep in the table was reached, so searching a place by its exact name could come back empty. The pack SQL now orders whole-query name matches first, ahead of the cap.
  • โœ… State downloads show a heads-up progress card (2026-07-07, device-verified). A routing-region download (and the place pack that follows it) now shows the same map heads-up progress card the Vela-voice download gets - "Downloading routing ยท N%" then "Saving places for offline search ยท N%" with a live bar - so a Settings-started state download stays visible after backing out to the map instead of running invisibly.
  • โœ… Quiet offline indicator, no banner (2026-07-07). When there's no connection, instead of a card hanging over the map Vela shows two subtle cues: a grayed globe-with-a-line-through-it icon + "Offline" in the search bar (before the settings gear, only on the bare map), and a small globe-slash chip tucked just under the category chips, next to the search box. Driven by a reactive ConnectivityManager default-network callback (MapUiState.offline), seeded on launch and updated on every network change (fails safe to "online"). The old "Offline results" status line was removed - these two cues already say it. Device-verified in both the search bar and on the map. Graceful when there's nothing to show (2026-07-07): searching with no connection (or on a wifi that's connected but has no real internet, so the Google call fails with a host/timeout error) and no downloaded area now shows the plain guidance "You're offline. Download an area in Settings โ–ธ Offline to search without a connection." instead of a raw "Unable to resolve host" error (isConnectivityError maps DNS/route/timeout/SSL failures to the offline message)
  • โœ… Open building-footprint overlay (Microsoft, ODbL - 2026-07-04, device-verified). Fills the map's building gaps where OSM is thin (a suburb the Microsoftโ†’OSM import never reached). Online it STREAMS automatically - no download to see houses (2026-07-05): the overlay is a PMTiles archive, so MapLibre reads the region-in-view straight from its hosted URL via HTTP range requests (only the visible tiles, a few KB), and footprints appear as you pan - verified device-side against downtown + suburban Reno streamed from nevada.pmtiles. Downloading a map area still saves that region's building overlay for offline use (renders from the local file with no signal); the overlay is Microsoft building footprints, baked off-device into a per-region .pmtiles archive by CI (scripts/build-overlay-region.sh โ†’ tippecanoe -l building -Z14 -z16, published to the building-overlays GitHub release + a signed-shape manifest, matrix workflow .github/workflows/building-overlays.yml). World catalog (tools/overlay-regions.json, 361 rows, ~250 base regions): 51 US states + DC (Microsoft US Building Footprints, one .geojson.zip each) plus ~185 world countries (Microsoft Global ML Building Footprints, quadkey GeoJSONL from global-buildings/dataset-links.csv)
  • one build script, two sources, chosen per catalog row. Big countries (>1500 MB, incl. India/Brazil/Russia/ Germany/Japan/โ€ฆ18 of them) are split into sub-national CHUNKS by quadkey prefix (India โ†’ 24 pieces), each with its own bbox so the app's smallest-covering-box rule downloads just the piece covering you - keeping every file under GitHub's 2 GB limit and downloadable in reasonable sizes (361 total regions, dispatched by group since that's over GitHub's 256-job matrix cap). The app's OverlayTileStore (a single-file sibling of RoutingGraphStore) downloads it into filesDir/overlays/<id>.pmtiles; VelaMapView adds a pmtiles://file://โ€ฆ VectorSource + a FillLayer beneath the OSM building layer (themed to the same fill/outline as OSM), so footprints only fill the gaps where OSM is thin - a suburb the Microsoftโ†’OSM import never reached. Same location-aware "smallest covering box" rule as routing; best-effort + silent (a background enhancement). Fixed en route: the 197 MB download was silently aborting at the shared OkHttp client's 12 s callTimeout (a scrape bound) โ†’ OverlayTileStore and RoutingGraphStore now use a derived callTimeout(0) download client (the same fix voice models already had).
  • โœ… Open house-number overlay (OpenAddresses - 2026-07-05, device-verified). The Microsoft footprints have geometry but no addresses, so house numbers come from a second streamed overlay: OpenAddresses address POINTS (per-source open/government licenses) baked into a per-state .pmtiles and rendered as a SymbolLayer of the number field ON TOP of the footprints, matched to the basemap vela-housenumber style (Noto Sans, gray + white halo, minZoom 17.5 so numbers appear only at street level, matching the basemap layer, collision thins dense blocks). Streams online exactly like the building overlay (pmtiles://https://โ€ฆ, HTTP range requests - no download). Device-verified: real house numbers in an OSM-thin suburb render over the footprints - filling the exact gap where OSM has no addr:housenumber. US states hosted; a CI pipeline (scripts/build-address-region.sh, tools/address-regions.json) fans out the 42 US states with an OpenAddresses statewide source, same pattern as the building overlay.
  • โœ… Offline routing - on-device, DONE 2026-06-30 on GraphHopper CH graphs; since 2026-09-15 the OsmAnd obf engine over a 414-piece world catalog, GraphHopper retired
  • โฌœ Region downloads as portable PMTiles + historical traffic

Platform & distribution

  • โœ… Optional generic app name (issue #226, 2026-08-03). Settings, Appearance, "Call it just Maps": the launcher entry flips to a plain Maps label for a stock look, and back. Deep links, shares and the geo: handler are untouched either way; a pinned home shortcut may need re-adding after switching since Android renames apps per launcher component.
  • โœ… Smoother drive start from a place's page (2026-09-30). Start on a place's page used to fly into the drive while the new route's own background work was still running, and the map dropped to a few frames a second; it now waits for that work (at most 3 s), like a start from the route preview. The drive's first spoken lines (the opener and the first turns' prompts) are prepared while the route preview is up, so the start from the preview runs at 41-51 fps where it ran 38-44.
  • โœ… Search without Google finds Vela's places (2026-09-29, #626). With "Use Vela without Google" on, search looks through Vela's own places, the ones the map draws, whether the region is downloaded or not (streamed from the same host as the map, a few small requests), plus the place packs. A category search ("Restaurants") is answered from that alone; names and addresses still ask the OpenStreetMap geocoder too, after Vela's own matches. Before, only the geocoder answered, and it knows no categories.
  • โœ… Ask before ending navigation (2026-09-29, #624). Settings > Navigation, off by default: the red X and Back during a drive ask "End navigation?" first.
  • โœ… Place packs for every piece of a split country or state (2026-09-29). Northern California, the German states, the French regions and the other split regions use their parent's place pack for offline search and offline addresses; "Get places" used to do nothing on them. A big parent pack (Germany's is 1.9 GB) is not pulled with a piece: its row says whose it is and how big, and "Get places" fetches it.
  • โœ… Offline search finds the places on the map, nearest first (2026-09-29). With no signal, search reads the downloaded places the map draws (Overture, AllThePlaces and OSM) around where you are looking, beside the OSM place pack, so a restaurant you can tap is one you can find. The pack's own results are the nearest ones now: they used to be the first 400 matches in file order, so "Restaurants" came back with places far away. A category search keeps to 100 km.
  • โœ… Long offline drives (2026-09-29). Downloaded regions now carry OsmAnd's highway hierarchy, precomputed shortcuts between the main roads, so a long drive with no signal routes in under a second: 256 km across North Rhine-Westphalia in 0.5 s, where it used to run out of memory, and 148 km across Delaware in 0.7 s on a Pixel 4a. Update a region to get it. Avoiding highways or tolls works on long drives too (161 km avoiding highways in 0.6 s), and so do long bike rides once a region is rebaked with the bicycle shortcuts (76 km that used to fail, 0.6 s). Walking uses the plain search as before.
  • โœ… Documentation site (2026-09-29). Every doc in the repository, the FAQ, the book, the full specification, the feature list, the roadmap and the contributor guides, published as one searchable site at pimpinpumpkin.github.io/Vela/docs/, with light and dark themes and no requests outside the site. The landing page has a Docs link and a search box that opens the results there. Rebuilt from main whenever a doc changes.
  • โœ… Project website (2026-07-15). A single-page showcase at https://pimpinpumpkin.github.io/Vela/ - hero with a live nav mockup, feature grid, the privacy pitch, a screenshot strip and download paths (direct APK / Obtainium / build it). Lives in site/ (self-contained HTML/CSS/JS, no external requests, screenshots are the public Davis set as optimized webp). It deploys through the SAME Pages artifact as the F-Droid repo and the map fonts (fdroid-repo.yml copies site/ to the artifact root) - a separate Pages workflow would wipe /repo and /fonts, so don't add one. Site-only pushes skip CI (paths-ignore) and trigger the F-Droid workflow directly instead.
  • โœ… Guidance volume (2026-08-08, issue #245) - Softer/Normal/Louder/Loudest tiers in Settings > Voice so spoken directions can cut through music; louder tiers boost the Vela voice's own audio, system voices can only be made softer.
  • โœ… Honest avoid toggles (2026-08-08) - the route picker says when avoid tolls/highways could not be applied, and the on-device avoid attempt is time-bounded so the chooser can never hang on it. Since 2026-09-06 the online route honors the toggles too (Google's keyless directions carry the avoid flags), so the note is rare: it appears only for a multi-stop route or when the on-device engine had to answer without the avoid profiles.
  • โœ… Results rank from you (2026-08-08) - when searching where you are, the result order and distances are computed from your location instead of the viewport center, so the list stops reshuffling as you pan; searching a far city keeps map-center ranking.
  • โœ… In-app updater (2026-07-08, user request). The PipePipe pattern: Vela checks the newest GitHub release (about once a day on launch, or on demand from Settings โ†’ Version โ†’ Check for updates), and when the release is newer than the running build a card on the map offers it. Update downloads the APK with a progress bar and hands it to Android's normal package installer, which verifies it is the same app signed with the same key before anything installs - the updater can never sideload something the platform would not accept as an update. "Not now" silences that version until a newer one appears. The launch check is a Settings toggle (on by default); Obtainium users can turn it off or just keep using Obtainium, both work. All localized.
  • โœ… No Google Play Services anywhere
  • โœ… Material 3 Compose UI; Hilt DI; R8 release builds
  • โœ… Public GitHub repo + local mirror + offline bundle
  • โœ… CI (GitHub Actions), three channels since 2026-08-07: pushes to main and canary build + test + sign only; a daily cron publishes the nightly prerelease (v0.4.<run>, versionCode 2000+run) when main moved, with an on demand dispatch for urgent fixes; a weekly job promotes the newest nightly to the stable release (same APK, no rebuild); and every push to the canary branch replaces the APK on the rolling canary release. Obtainium tracks stable with zero config; nightly users flip on "include prereleases"; canary rides the in-app updater (Settings > About > Update channel, three-way picker) or a manual download from the canary release page. The in-app updater follows the picked channel
  • โœ… Every download can be canceled, and the area download has ONE banner (2026-07-23). Voice models, dictation engines, region graphs, place packs, the update APK and the map-area tile save all show a Cancel wherever their progress appears (map cards and every Settings site); canceling aborts the transfer within a chunk, cleans the partial file, and stays quiet (no "failed" toast). The "download this area" save also stopped flashing its raw-coordinate status line on every tick - progress now lives in one card and completion is a single localized flash.
  • โœ… Downloads survive the background (issue #212) - voice models, dictation engines, region graphs, place packs, building overlays, offline place data and the update APK all run on an app-lifetime scope with a refcounted dataSync foreground service (a quiet "Downloading X" notification) holding the process alive, so switching apps or swiping Vela from recents no longer kills an in-flight download (OEM background killers were reaping the process seconds after a Home press; downloads also used to die with the ViewModel). app/download/DownloadService + MapViewModel.downloadLaunch
  • โœ… Opt-in diagnostics / debug export (Settings โ†’ Diagnostics, off by default) - a local-only event log (searches, computed routes, parser "drift", nav start/reroute/arrival) that the user can Export debug session to a JSON bundle and hand to a developer via the system share sheet. Never auto-uploaded - user-initiated and user-routed; turning it off wipes the log; in-memory only (capped at 300 events). The no-backend half of the telemetry plan; core/diag/DiagLog + app/diag/DiagExporter, consent dialog on enable, PRIVACY.md updated. Redact places in exports (same screen, 2026-09-15, issue #507): the export with coordinates at ~10 km instead of ~1 km and the searches, destinations, links, cids and review page text replaced by [redacted], for a report meant to be posted publicly; timing, counts, zoom levels and error text stay
  • โœ… Crash capture - an uncaught-exception handler (app/diag/CrashCatcher, installed in VelaApp) persists the stack trace + breadcrumbs + app/device versions to disk, surviving the restart, so after a crash the user can Export crash report from Settings โ†’ Diagnostics (the fix for "nav crashed but the phone wasn't tethered, no logcat"). Captured even with diagnostics off (a stack trace is benign + local); never auto-sent; chains to the system handler so normal crash behavior is unchanged
  • โœ… Trip recording + replay (Settings โ†’ "Save my trips", off by default, separate opt-in - more invasive than diagnostics since it's your exact routes; never uploaded) - records each navigation's GPS trace to a local file (app/replay/TripStore), so a drive can be replayed on the map at 3ร— to test turn-by-turn without driving it again (LocationProvider.replay feeds the recorded fixes through the same nav/camera/dot pipeline). A replay shows a "Stop replay" button on the map and, when it finishes (or is stopped), live GPS resumes automatically - earlier the live feed never came back until the app was restarted. The trip is saved the instant you arrive, not only when you tap "Done", so it survives if you leave the arrival card. Recorded trips are listed in Settings (newest first) with their recorded date + point count and Replay / Share / Delete; the list refreshes on entry and shows an empty-state hint while recording is on but nothing's captured yet. Share exports the raw CSV trace via the system sheet (same FileProvider as the diag/saved-places export) - so a drive can be pulled off a release build and handed over for replay/debug without a dev build (this is how the 2026-06-20 early-arrival bug was diagnosed). (Bigger picture - opt-in encrypted upload of traces for remote debugging is the natural next step as the tester pool grows; see ROADMAP's opt-in-telemetry big bet. Location traces are the most sensitive data the app touches, so: explicit per-share consent, client-side encryption to a key only the dev holds, minimal/redactable payload, self-hosted or pre-signed endpoint, auto-expiry.) Both opt-ins now live only in Settings โ†’ Diagnostics (the first-run prompt that offered them was removed in the 2026-07-10 onboarding declutter; the diagnostics ask instead surfaces in context on the crash-report card). Replay now auto-routes to the trip's destination and starts turn-by-turn for you (best-effort - the trace still plays if routing fails - and the auto-started nav is torn down when the replay ends), so you no longer have to start nav manually first. (Pending on-device verification.)
  • โœ… Simulate driving (demo mode) (Settings โ†’ Diagnostics โ†’ "Simulate driving (demo)", off by default) - tapping Start on any planned route drives it as a synthetic GPS trace instead of using the real fix, so navigation runs anywhere (a Davis route while the phone sits in another state) - for demos, screenshots and testing turn-by-turn without leaving the couch. It reuses the recorded-trip replay pipeline wholesale: DemoTrace.fromRoute (pure :core) walks the route polyline at a constant cruise speed emitting one clean ReplayFix/sec (real bearing + speed + monotonic time), and LocationProvider.replay feeds those through the exact live-nav loop (hermetic NavSession.replayMode, puck physics, follow-camera, spoken + haptic guidance, speed-limit badge). Unlike a recorded trace it's meant to look like real nav - no "Stop replay" pill; the normal End button stops it (stopNav cancels the demo job, which tears the session down and resumes live GPS). This is how the 05-navigation screenshot was shot. Turn it off to navigate for real.
  • โœ… Simulate my location (demo mode) (Settings โ†’ Diagnostics โ†’ "Simulate my location (demo)", off by default) - a sibling of simulate-driving for the browse map: flip it on and Vela pretends you're standing at the current map center, so the location dot, the directions origin ("Your location"), and recenter all read from there instead of your real GPS. Lets the app be shown or photographed from anywhere without leaking a real position (ui/SimLocation.kt, a process-wide reactive holder persisted to vela_settings; MapViewModel pins myLocation to the captured point and suspends the live GPS collector, resuming it when you turn the toggle back off). It's how the Davis/Sacramento screenshots were taken from another state. Turn it off to use real GPS.
  • โœ… Settings shows the installed app version (name + build code)
  • โœ… Full D-pad / no-touchscreen operation (touch is a bonus, not required) - every surface reachable and activatable by focus traversal with a visible focus ring; the map itself is key-drivable (arrows pan, OK "taps" a center crosshair, hold-OK long-presses/drops a pin, on-screen +/โˆ’ zoom buttons); key alternatives for every gesture (banner step preview โ†/โ†’, sheet-handle OK-toggle, photo-viewer paging); the search overlay's focus traps fixed. A full-function sweep (2026-07-07) exercised every surface end-to-end by D-pad and closed the last gaps: text fields escape UP/DOWN via dpadFieldEscape (Settings + reviews search), the search field's DOWN drops into the suggestions, Choose-on-map keeps the map pannable to place the pin, and the directions panel scroll-caps so Start is reachable with 4 alternates (the last also fixes a touch layout bug). Every screen/sheet/panel auto-focuses a sensible element on open (rememberDpadAutoFocus), so no keypress is wasted "waking up" focus - and because a Compose DropdownMenu/AlertDialog can't be pre-focused (framework limit, ~10 approaches proven), menus and dialogs were rebuilt as VelaMenu (anchored DropdownMenu under touch, auto-focusing raw-Dialog chooser under D-pad) and VelaDialog (auto-focusing raw-Dialog), so those land focused too. Affordances appear only in key-driven input mode (rememberDpadMode), so touch UX is unchanged. Design/findings/merge policy: docs/dpad.md
  • ๐ŸŸก F-Droid: Vela's OWN repo is live (see FDROID.md - add it to any F-Droid client); official f-droid.org inclusion + reproducible build still open
  • โฌœ UnifiedPush for delay alerts (no FCM)
  • โฌœ ACRA / self-hosted crash reporting

Resilience / maintainability

  • โœ… Remotely-updatable scraper calibration - calibration.json at the repo root holds the pb templates, endpoint URLs and (phase 2) the search parser's positional field-index paths (name, address, rating, photos, โ€ฆ as [i,j,โ€ฆ] arrays). The app fetches it at launch and adopts a newer version (host-allowlisted to google.com) without an app update - so most Google drift (a moved pb, endpoint, or field index) is now a one-line edit + version bump, not a release.
  • โœ… Signed update channel - the bundle is ECDSA-P256/SHA-256 signed (calibration.json.sig); the app verifies it against a pinned public key before adopting, so a repo/CDN compromise can't push config - or code - to devices (private key kept out of the repo; scripts/sign-calibration.sh re-signs; BundleSignature.verify is unit-tested). (verified on-device)
  • โœ… Pushed notices - a notices array in the signed bundle surfaces dismissable alerts on the bare map ("search is down, fix coming") with no app update; dismissals persist per-id. (verified on-device end-to-end)
  • โœ… Remote parse logic (phase 3) - a signed transformsJs bundle runs in a Rhino sandbox (interpreted, no Java access), so a response-shape change can be hot-fixed too - not just a moved field. Two search hooks (parseSearch full re-parse / transformPlaces post-process) over a flat place-JSON contract; compiled Kotlin is always the fallback. (verified on-device: a pushed transform marked the first result, then cleared; engine + sandbox + contract unit-tested)

Known calibration debts (the NewPipe lifestyle)

  • Google request/response shapes are pinned to a 2026-06-15 capture; expect periodic re-calibration (paths documented in the README). Pb/endpoint drift is now a remote calibration.json fix (above); field-index paths are remote too (phase 2, the paths object) - so index drift is also just an edit + version bump, no build.
  • EU/EEA consent wall: pre-seeds Google's SOCS/CONSENT cookies in the shared jar so a cookieless session isn't bounced to consent.google.com (best-effort, US-verified only; the full form-POST handshake is the follow-up if it persists).

Added 2026-07-11 (sheet-polish round)

  • Destination side on arrival: the approach banner and the spoken arrival say "Your destination is on the left/right" when the router knows it (OSRM arrive modifier), in all supported languages.
  • Sticky travel mode: the last-used mode is the default for the next directions session (parked-car routing still forces Walk).
  • Zoomed-out searches cover the visible viewport instead of a fixed ~25 km window.
  • Results-sheet filters drop the matching map pins, not just the list rows.
  • Native rating histogram on the Reviews tab, scraped in passing by the photo walk.
  • Menu photo tiles carry their upload date (RPC dates joined onto the categorized walk).
  • The route chooser drags from anywhere with inertia; travel-time picks are Material 3 dialogs that refuse past times; search-along-route chips are filled action chips.
  • Save is a bookmark icon; sheet headers share one circled-button language (place sheet, directions close, results chevron and close, recent-search remove).

  • โœ… Hebrew (ืขื‘ืจื™ืช): the 15th language and first RTL locale (2026-07-09/11, contributed by The-Young-boy; landed 2026-07-12 alongside the Chinese/Japanese work). All three i18n layers cover Hebrew: the ~531 UI strings (in res/values-iw/strings.xml - iw is AAPT's legacy Hebrew resource qualifier - every format-placeholder validated against English), the generated spoken nav voice (HeNavStrings in :core, masculine-singular Waze/Google-Maps register: "ืคื ื” ื™ืžื™ื ื”", "ื‘ืขื•ื“ 300 ืžื˜ืจ", feminine roundabout-exit ordinals "ื‘ื™ืฆื™ืื” ื”ืฉืœื™ืฉื™ืช", "ื”ื™ืขื“ ืฉืœืš ืžืฆื“ ืฉืžืืœ/ื™ืžื™ืŸ"), and the Google POI scrape (hl follows the locale; open/closed keyword table keyed under both iw and he). Registered under he in the picker/NavStrings: on JDK 17+ (useOldISOCodes=false) the resolved code is he, the resources live in the legacy values-iw folder (the platform maps a he locale onto it), and NavStrings.forLanguage accepts both he and iw (Android hands back the old code on-device). RTL is automatic: the app already declares android:supportsRtl and re-creates the Activity with a Hebrew config, so Compose's LayoutDirection flips (a whole-app scan found no hardcoded left/right, Absolute arrangements or forced LayoutDirection). Unit-tested.

  • โœ… Community translations by pull request (2026-07-14; Weblate still pending, see issue #285). Anyone can translate or fix UI strings by editing one values-<lang>/strings.xml in the GitHub web editor and opening a PR - no git client, no Android toolchain. See docs/TRANSLATING.md for the flow and the rules (placeholders, CLDR plural categories, no em dashes). New strings are added to the English base only, and an untranslated string falls back to English, so partial contributions are safe. Hosted Weblate is the intended home and is NOT live: it asks that a project be established before taking it on and Vela is too young to qualify yet, so the docs describe the PR flow until that changes.

Added 2026-07-17 (offline routing on pre-Android-14 devices)

  • โœ… Downloaded routing graphs now load on Android below 14 (API 34). (Historical: the graphs and this machinery were retired on 2026-09-15 with the move to the obf engine.) Three pre-API-34 gaps in the GraphHopper offline engine meant a downloaded graph loaded on API 34+ only, and silently failed everywhere below (never caught because the :ghprobe test device was on Android 14): the custom model is now built programmatically instead of via the jar's Jackson record probe (Class.getRecordComponents is API 34+), profiles are set after init() to skip a second Jackson round-trip, and a build-time ASM transform rewrites MMapDataAccess's JDK-13 absolute-bulk ByteBuffer calls to an API-1 shim. Offline routing now works down to minSdk 26. Fix contributed by ars18 (vela-dpad fork); a unit test guards the hand-built model against GraphHopper-upgrade drift.

Added 2026-07-16 (community QOL batch, issues #169 #170 #171 #172 #173)

  • โœ… Edit the destination from the route picker (issue #170). Both endpoint rows on the directions top card are editable now: the your-location end keeps the origin picker, and tapping the place end opens the same search overlay to swap the destination in place, keeping the custom origin, stops and travel mode (backing out used to lose the other end). "Choose on map" works for the destination too, with its own crosshair labels.

  • โœ… Read + copy a full business name (issue #169). A very long place name (CJK businesses especially) clips at two lines in the sheet header; tapping the name now toggles the full name, long-pressing copies it to the clipboard, and the share menu carries a "Copy name" item (also the key-only path on D-pad phones, where the name is its own focus stop).

  • โœ… Saved places show on the map while browsing (issue #171). Every list place and quick-save draws a small ringed disc in its list's color with the list's icon (or emoji) on the browse map; tapping one opens the place. Hidden while search results, navigation, a replay or Street View own the map. Sparse deliberate objects, so they always render (no collision fade).
  • โœ… Settings search (issue #172). A magnifier in the Settings top bar opens a floating search field; every section title and row label self-registers its position as it lays out, so matches are complete with no hand-kept keyword table, and picking one scrolls straight to the row. D-pad: the field auto-focuses, DOWN escapes into the results, BACK closes the panel first.
  • โœ… Custom emoji icons on lists (issue #173). The list editor's icon row grows an emoji tile: a curated picker grid (works on key-only phones) plus a free-type field for any emoji. Stored in the existing icon field as "emoji:X", so old payloads and old builds fall back to the bookmark; the emoji renders in list rows and as the saved-place map pin.
  • โœ… Tapping a place keeps your zoom (issue #636). Opening a place pans it above the sheet but no longer zooms the map out to the search level, so you can tap through places at street zoom; from farther out than that it still zooms in.
  • โœ… Links from other apps can open directions or start the drive (discussion #640). Settings > Navigation > Links from other apps: show the place, open directions, or start navigation. For delivery apps that send one stop after another.
  • โœ… Alternate routes stand out (discussion #639). Alternates draw in a faded blue with a darker outline instead of gray, so they no longer disappear into a dense street grid.
  • โœ… 12-hour or 24-hour clock (discussion #637). Settings > Appearance > Clock follows the phone or forces either; transit directions and stop boards from Google follow it too.
  • โœ… Typed addresses found up close (issue #638). A street address typed while zoomed in on a busy area is looked up even when the nearby businesses' ZIP codes or numbers happen to contain its digits.
  • โœ… Google Sans Flex, app and map (2026-09-30). The app's text and the map's labels are drawn in Google's openly licensed font by default. Settings > Appearance > Font switches the app back to the system font or to a font file of your own.
  • โœ… 3D buildings rise when you tilt (2026-09-30). Looking straight down, the map shows flat footprints and clear streets; tilt it and the buildings fade up, with walls shaded darker than roofs.
  • โœ… Route preview shows the trip alone (2026-09-30). With the route chooser up, no places, transit icons or saved pins draw; the destination keeps its own place pin and name instead of a flag.
  • โœ… Saved routes (issue #622). Pick a route in the route chooser and use โ‹ฎ > Save this route. Whenever you ask for the same trip again (same mode, same start area, same destination), your route shows up next to the others with a live-traffic time, labeled "Your route", and driving it keeps you on it through reroutes. Rename or delete them in Settings > Saved places. A trip with stops can be saved as a RUN ("Stop at these places" in the save dialog): the milkman's round, started in one tap from "Your routes" on the search page with every stop loaded. A trip takes up to 10 stops. Your saved route leads the route list, already selected, with a colored "Your route" chip; open one from your saved routes and the โ‹ฎ menu offers "Save changes" to edit it. Pin saved places and routes to the search page; everything else lives behind the bookmark button. After a drive where you went your own way, the arrival card offers "Save the way you drove".
  • โœ… Per-place map icons (issue #629). A saved place can wear its own icon instead of its list's: the place sheet's save menu has "Choose icon" (the list editor's glyphs and emoji picker) and "Use the list's icon" resets it. The pin keeps the list's color.
  • โœ… Hebrew open/closed status fix (issue #95 diag). Google prefixes Hebrew status strings with "ื”ืžืงื•ื" ("the place"), so the bare closed/open words never matched at the start of the string and every Hebrew place drew no status color at all. The keyword tables now carry the prefixed forms, pinned by tests against the exact strings from a live device diag.
  • โœ… Remote keyword tables actually wired. The signed calibration bundle's word-table overrides (status open/closed words, transit gate + exclusion words, departure-board indices) were parsed nowhere: the fields existed and the app pushed them into the parsers, but the bundle parser silently dropped them, so every one of those overrides was dead. They now parse leniently like the tuning dials (bad entries skipped, empty means absent).
  • โœ… No-GPS search bias guard. A device that never gets a location fix (WiFi tablet) sits on MapLibre's virgin camera near 0,0 and every search was biased to open ocean. A bias point within about 50 km of null island is now discarded so regional ranking wins instead.
  • โœ… Full review text in the inline list (issue #181). Long reviews rendered with a data-side "โ€ฆ" because a card is often harvested on the same tick its More toggle is clicked, before Google's async re-render swaps in the full body, and the accumulator kept the first capture forever. A longer text for a known review now replaces the stored entry, and the More click uses the button's class hook so it works in every UI language (the old label match only knew English). Device-verified on a busy coffee shop: multi-paragraph reviews end in real sentences.
  • โœ… Foreign street names are spoken, not skipped (issue #184). When guidance is in one language but a road name is in another script (Hebrew "ืจื—ื•ื‘ ื”ืจืฆืœ" while you drive with the English voice), the name used to vanish, because a single-language voice has no phonemes for those glyphs and drops them. The spoken string now romanizes foreign-script name runs into the voice's alphabet (ICU, built into Android), the way Google reads "onto Rehov Herzl", while the on-screen banner keeps the real local-script name that matches the street sign. It only touches the runs the voice can't say (existing Latin text and its accents are left alone) and only for Latin-script voices (a Hebrew or Russian voice reads its own script natively). CJK is deliberately left to the native Japanese/Chinese voices, since ICU mis-reads kanji as Chinese pinyin; a proper kanji-to-romaji step is a possible follow-up.
  • โœ… No stale voice-prompt burst when a voice attaches late. If you drove with no voice installed (nav queues prompts with nothing to speak them) and then installed one, the whole backlog used to flush at once, a burst of out-of-order directions. The queue now speaks only the most recent line on attach and is cleared whenever guidance stops, so installing a voice can't trigger a stale replay.
  • โœ… Install the Vela voice straight from Voice settings. When no Vela voice is installed, the one-tap install button now sits in the Voice settings section itself, not only inside the Voice library, so you don't have to dig for it.
  • โœ… Search your own history and lists as you type (issue #180). As you type, matches from your recent searches, recently-viewed places, and saved lists surface at the top of the suggestions, above Google's, each with an icon that tells them apart (clock for a past search, pin for a place you opened, bookmark for a saved-list place). It is computed on-device, so it is instant and the only thing that shows when you are offline. A network suggestion that duplicates one of your own is dropped (matched by name and location, since saved places carry no Google id). Tapping a place opens it; tapping a past search re-runs it. Press-and-hold any suggestion (or tap its โ‹ฎ) to save a place into one of your lists or drop a history row, the same list picker the place sheet uses. Looking up one location out of a chain no longer means re-typing the city.
  • โœ… Search your contacts (issue #243, 2026-08-08; reworked 2026-09-06). Opt-in (Settings > Search, off by default; the contacts permission is asked when you flip the toggle, never at install). Typing a contact's name suggests each of their saved addresses at the top of the results as a person: their photo (or a person glyph on the same tinted disc Home and Work use) and a "Contact ยท Home" or "Contact ยท Work" chip from your address book's own label. Picking one looks up the address and opens it under the contact's name with the address beneath, so the place sheet, Save, Directions and Recents all say whose place it is, and typing the name again next week finds it in history. The same rows work in the directions "To" and "From" fields, as a stop, and in the Android Auto search. Matching happens entirely on the phone against an in-memory copy of the address-bearing contacts, so it is instant and works offline (the address itself geocodes from a downloaded region when there is no signal); only the address string is ever sent to a geocoder, never the name or the list.