From 1934a38677cf175e44390049b6ae6c17609e224c Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Thu, 18 Jun 2026 08:00:39 +0100 Subject: [PATCH] Smal --- frontend/src/i18n/locales/en.ts | 2 +- frontend/src/lib/url-state.test.ts | 26 ++++++++++++++++++++++++++ frontend/src/lib/url-state.ts | 22 +++++++++++++++++----- server-rs/src/data/property/loading.rs | 20 +++++++++++++++++++- server-rs/src/data/property/mod.rs | 5 +++-- 5 files changed, 66 insertions(+), 9 deletions(-) diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 9886757..8280b60 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -917,7 +917,7 @@ const en = { body: 'Want to see property data around you? Use your location, or start the demo at Canary Wharf.', useLocation: 'Use my location', locating: 'Locating…', - useDefault: 'Show Canary Wharf', + useDefault: 'Show London', }, // ── Mobile Drawer ────────────────────────────────── diff --git a/frontend/src/lib/url-state.test.ts b/frontend/src/lib/url-state.test.ts index 5c48d55..4808ab0 100644 --- a/frontend/src/lib/url-state.test.ts +++ b/frontend/src/lib/url-state.test.ts @@ -196,6 +196,32 @@ describe('url-state', () => { expect(state.overlays).toEqual(new Set(['noise', 'crime-hotspots'])); }); + it('enables crime + trees overlays by default on a fresh visit', () => { + const state = parseUrlState(); + + expect(state.overlays).toEqual(new Set(['crime-hotspots', 'trees-outside-woodlands'])); + }); + + it('round-trips an explicit "all overlays off" via the __none sentinel', () => { + const params = stateToParams( + null, + {}, + [], + new Set(), + 'area', + undefined, + undefined, + new Set() + ); + + expect(params.getAll('overlay')).toEqual(['__none']); + + window.history.replaceState({}, '', `/?${params.toString()}`); + const state = parseUrlState(); + + expect(state.overlays).toEqual(new Set()); + }); + it('round-trips satellite basemap selection', () => { const params = stateToParams( null, diff --git a/frontend/src/lib/url-state.ts b/frontend/src/lib/url-state.ts index c8d18f4..20bfe22 100644 --- a/frontend/src/lib/url-state.ts +++ b/frontend/src/lib/url-state.ts @@ -48,7 +48,7 @@ import { type PoiFilterName, } from './poi-distance-filter'; import { dedupeTravelTimeEntries } from './travel-params'; -import { isOverlayId, type OverlayId } from './overlays'; +import { isOverlayId, DEFAULT_OVERLAY_IDS, type OverlayId } from './overlays'; import { CRIME_TYPES, isCrimeTypeValue } from './crime-types'; import { isBasemapId, type BasemapId } from './basemaps'; import { @@ -59,6 +59,7 @@ import { const POI_NONE_PARAM = '__none'; const CRIME_TYPES_NONE_PARAM = '__none'; +const OVERLAY_NONE_PARAM = '__none'; const CRIME_OVERLAY_ID: OverlayId = 'crime-hotspots'; export interface UrlState { @@ -227,7 +228,7 @@ export function parseUrlState(): UrlState { hasExplicitView: false, filters: parseFilters(params), poiCategories: new Set(), - overlays: new Set(), + overlays: new Set(DEFAULT_OVERLAY_IDS), basemap: 'standard', colorOpacity: DEFAULT_COLOR_OPACITY, tab: 'area', @@ -266,9 +267,14 @@ export function parseUrlState(): UrlState { } } + // Overlays default to DEFAULT_OVERLAY_IDS on a fresh visit; explicit `overlay` + // params override that, and the `__none` sentinel records "all off" so it + // survives a reload instead of silently restoring the defaults. const overlayParams = params.getAll('overlay'); if (overlayParams.length > 0) { - result.overlays = new Set(overlayParams.filter(isOverlayId)); + result.overlays = overlayParams.includes(OVERLAY_NONE_PARAM) + ? new Set() + : new Set(overlayParams.filter(isOverlayId)); } // Crime-type filter: repeated `crimeType` params, or the `__none` sentinel. @@ -451,8 +457,14 @@ export function stateToParams( } if (selectedOverlays) { - for (const overlay of selectedOverlays) { - params.append('overlay', overlay); + if (selectedOverlays.size === 0) { + // Distinguish a deliberate "all overlays off" from a fresh visit, which + // would otherwise re-apply the defaults on reload. + params.append('overlay', OVERLAY_NONE_PARAM); + } else { + for (const overlay of selectedOverlays) { + params.append('overlay', overlay); + } } } diff --git a/server-rs/src/data/property/loading.rs b/server-rs/src/data/property/loading.rs index af006b4..29d224d 100644 --- a/server-rs/src/data/property/loading.rs +++ b/server-rs/src/data/property/loading.rs @@ -827,6 +827,18 @@ impl PropertyData { } } } + + // Reclaim geometric-growth slack. `address_search_token_keys` and the + // posting lists are built by repeated `push`, so their capacity rounds up + // to the next doubling step; at full UK scale (88M row-token keys, 39M + // postings) that slack is ~250MB of permanently-resident waste. These + // tables live for the whole process, so shrinking once at build time is + // a pure win with no behaviour change. + address_search_token_keys.shrink_to_fit(); + for postings in address_token_index.values_mut() { + postings.shrink_to_fit(); + } + // Keep every distinctive token: common road words ("high", "church", "station") are // exactly what people search, and dropping them made those roads unsearchable while a // prefix fallback surfaced the wrong street ("Highbury" for "High"). The candidate scan @@ -838,7 +850,10 @@ impl PropertyData { .max() .unwrap_or(0); let address_prefix_index = build_address_prefix_index(&address_token_index); - let address_search_interner = address_search_rodeo.into_reader(); + // Resolve-only view: scoring only ever calls `.resolve(spur)`, never + // `.get(str)`, so we drop the string->key reverse map that a RodeoReader + // would keep (~tens of MB of hashmap we never touch). + let address_search_interner = address_search_rodeo.into_resolver(); let address_postings_count: usize = address_token_index.values().map(Vec::len).sum(); tracing::info!( tokens = address_token_index.len(), @@ -861,6 +876,9 @@ impl PropertyData { .or_default() .push(new_row as u32); } + for rows in postcode_row_index.values_mut() { + rows.shrink_to_fit(); + } let postcode_interner = postcode_rodeo.into_reader(); let row_to_poi_metric_idx: Vec = if poi_metrics.is_empty() { diff --git a/server-rs/src/data/property/mod.rs b/server-rs/src/data/property/mod.rs index adcb8ef..72a3b4d 100644 --- a/server-rs/src/data/property/mod.rs +++ b/server-rs/src/data/property/mod.rs @@ -88,7 +88,8 @@ pub struct PropertyData { /// Prefix lookup from typed address-token prefix to indexed full address tokens. address_prefix_index: FxHashMap>, /// Interned normalized address-search tokens used for per-row scoring. - address_search_interner: lasso::RodeoReader, + /// Resolve-only (no string->key reverse map): scoring only resolves keys. + address_search_interner: lasso::RodeoResolver, /// Flat per-row normalized address-search token keys. address_search_token_keys: Vec, /// Offset into `address_search_token_keys` for each row. @@ -243,7 +244,7 @@ impl PropertyData { postcode_row_index: FxHashMap::default(), address_token_index: FxHashMap::default(), address_prefix_index: FxHashMap::default(), - address_search_interner: lasso::Rodeo::default().into_reader(), + address_search_interner: lasso::Rodeo::default().into_resolver(), address_search_token_keys: Vec::new(), address_search_token_offsets: Vec::new(), address_search_token_lengths: Vec::new(),