# 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 [`docs/HOW-IT-WORKS.md`](docs/HOW-IT-WORKS.md). > > | Area | The short version | > |---|---| > | [Map & rendering](#map--rendering) | Keyless OpenFreeMap/Protomaps vector tiles, Google-style POI markers, hillshade, in-app light/dark, scale bar | > | [Search & POIs](#search--pois-live-google-data) | Live keyless Google search - name/rating/reviews/hours/price/website/photos/popular-times/"people also search for" | > | [Routing & traffic](#routing--traffic) | OSRM turn-by-turn (primary) + Google traffic ETA & jam reroute; alternates; **offline on-device routing** (135-region world catalog) | > | [Navigation](#navigation) | Maneuver banner with a real lane diagram + highway shields, spoken + haptic guidance, speedometer, re-center, arrival summary; **Android Auto (first cut)** | > | [Location](#location-degoogled) | AOSP LocationManager + rotation-vector heading - no GMS/Fused | > | [Offline](#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](#platform--distribution) | GrapheneOS/no-GMS, CI-signed nightly + weekly-stable `v0.4.` releases, Obtainium + own F-Droid repo, **in-app updater** | > | [Resilience](#resilience--maintainability) | Signed remote calibration (pb/paths/JS) + notices - hot-fix drift without an app update | ## Map & rendering - โœ… **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 labelled 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 (metres 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 centre, 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 neighbour) 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, cannot misfire on healthy devices (a successful render resets the counter), 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-coloured 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 colour 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-grey dark reading almost identical (user: "classic looks bluish like modern, houses too"). Classic dark is now a NEUTRAL charcoal-slate identity - slate land, GREY 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 colour hardcoded to Modern, so in Classic those houses stayed Google-navy while the OSM ones went grey; it now honours the palette too. Also, since the two sets currently diverge mostly in dark mode, the Settings hint says so. - โœ… **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 grey 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 colour, 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 coloured `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 **centre 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 centre, 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 centres 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** centres 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 greys out** behind the vehicle (Google-style) - a line-progress gradient on the route (grey 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 coloured **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 labelled 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 - โœ… **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 neighbour'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-coloured labels**, a **clean Google-style road treatment** - white road fills on light-grey land with the **casings faded out** (minor-road casing == the land, so streets are crisp white lines with **no outline**), soft-yellow motorways, **neutralised 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 recolour, 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 recolour live off one preference, `AppTheme`/`isAppInDarkTheme`; persisted). Dark mode recolours **every** landuse/landcover fill (commercial, school, retail, โ€ฆ), not just a hardcoded few, so no light/cream patches break the night palette (verified on-device) - โœ… **Google-style POI markers** - category-coloured 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 coloured 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 labelled, 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 - grey 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-grey TEARDROP/pin backing** (Google grey `#9AA0A6`) - a rounded body whose point extends below the coloured 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 coloured dot is the bitmap centre so the layer's default centre-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/grey dots pop on the grey 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 grey 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 neighbourhood still looks bare (that's an OSM data gap, not a Vela bug). - โœ… **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 neighbourhood doesn't re-hit the server. Zoom-gated (no setting); shows in both browse and nav. - โœ… **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 > Map > **Surveillance cameras** draws mapped automated licence-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 > Map 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: `OverpassAlprCameras.fetchAlong` counts the ALPR nodes within ~120 m of each route's polyline (reusing the `out body` fetch), 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 initialised 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-centre 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-centre 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 neighbourhood titles; water names stay (2026-07-17).** Hamlet and neighbourhood 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 neighbour 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 behaviour 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 neighbourhood 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-centres 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). - โœ… **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); honours 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_`). **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 ellipsising. **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("ยท")`) + ellipsised. 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 centre you're looking at** (`mapCenter ?: myLocation`), Google-style - so panning to another neighbourhood/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** serialised 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 serialisation 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-centred + 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 coloured 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 neighbourhood 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-grey 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 - โœ… **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-` icons as the OSM POIs (fork=food, cart=shop, fuel, etc.), with decluttered labels - so they read as native map POIs, not clashing red pins. Tap to open like any result. **Google-first balance:** while ambient Google POIs are showing, the OSM **business** POI layers (`poi_r1/r7/r20`) are 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 scraping stays 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 behaviour (2026-06-27):** 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 - โœ… Map rotation/tilt + heading-up mode during nav (tilted follow-camera, speed-adaptive zoom) ## Search & POIs (live Google data) - โœ… **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 `pb`s 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 centreline), 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, centred 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 neighbour graph is the base pano's), and they fetch the neighbour 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 categorised "Restaurant" is always kept), and it's PRECISE - food "โ€ฆbar" categories (sushi/juice/coffee/salad bar) stay. Because Google returns the category **localized** (`hl=`), 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**, colour-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 minimises; 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 colour-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`.) - โœ… 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 `Review`s. **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 coloured icon + name + count that opens as results. Create/edit via a dialog with a **name, a 10-icon picker and an 8-colour 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 grey 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 colour (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 colour. 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 colour 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 centres 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 labelled "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 colourless Text falls back to black; the row now uses the same on-surface colour as every neighbouring row in both themes. - โœ… **Material You dynamic colour (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 colours 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 greys and the map keeps its own colours - those are reading surfaces full of meaning-bearing colour (open/closed status, stars, the route line) that has to stay legible on every wallpaper. Worth knowing: Google Maps itself never adopted dynamic colour 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. - โœ… **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 colour-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 neighbour'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 behaviour; 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 - โœ… **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**. For drive/walk/bike it lists the **route options** - each with a **traffic-coloured 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)** - 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 - โœ… **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. 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 neighbour'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. - โœ… **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 metre "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!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; 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** (metres 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-grey 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** (labelled "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 greyed on the map + tappable** (Google-style) - the non-selected routes render as **grey lines beneath the active blue one** (theme-aware shade so they read on dark/light tiles); **tap a grey 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: grey 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 - โœ… **Pin stays visible above the sheet** - opening a place pushes the map's optical centre 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 on search, 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: Taco Bell (612 reviews) **3 โ†’ 50**, Pike Place Chowder **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//`, 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//` 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_` - libritts_r's 904 variants don't bleed onto single-speaker 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's 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. **Street numbers read the human way + no clipped start (2026-07-03):** three-digit street ordinals are now spoken as people say them - "**120th** Street" โ†’ "one twentieth Street", not the 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 direction" - the engine now **skips the DEPART entirely and advances silently** to the first real turn (which announces itself on approach, as Google does). *(Piper's audible in-breaths between clauses are a VITS model artifact, not tunable without a different model - a known limitation.)* **New default voice = HFC Female @ 0.8ร— (2026-07-03, calibration v10).** After auditioning the browser, 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_r. The default voice id 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_r (the multi-speaker voice) when someone picks it. Nav prompts 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 (2026-07-03, first step of app localization):** `PiperCatalog` generalized from US/GB-only to **any language** - it derives `langCode`/`region` from the voice id and the browser now **groups by language** (English first, then by endonym: Franรงais, 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 *pairing* (French UI + French nav text + a French voice, auto-suggested from your phone's language) lands with the [localization work](ROADMAP.md) in progress. **Spoken navigation now speaks 10 languages (2026-07-03).** The whole generated 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** (`AppLocale`, default = system): **English, French, German, Spanish, Italian, Portuguese, 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-colour 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-/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"=stop, German noun capitalization, French `%` spacing). **Google POI content now comes 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 request time (`GoogleMapsDataSource.localized()`, no-op for English โ†’ English users byte-identical), so **categories, hours, open/closed status, price and distances arrive localized** ("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` colours from that boolean, so an open place is green in every language. *(Correction 2026-07-04: the first 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 colour can never contradict the words.)* Permanently-closed uses the numeric `[23]` flag, which IS real. **Device-verified on a Pixel:** searched in French โ†’ 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 โ†” language mismatch handled (2026-07-04).** A Piper voice is a **single-language** model, so overriding the app/system language to one whose voice isn't downloaded made the *installed* voice read the new language - the English HFC voice literally reading Russian 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 `TextToSpeech` in the target language** instead (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 a <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` 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 `