From 55238f59aa9219950d5f11960a97451ce3d43ecf Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sat, 4 Apr 2026 22:59:07 +0100 Subject: [PATCH] Lint & small changes --- .../src/components/map/LocationSearch.tsx | 16 +- frontend/src/components/map/MapPage.tsx | 207 ++-- frontend/src/hooks/useDeckLayers.ts | 506 +++++--- frontend/src/hooks/useHexagonSelection.ts | 58 +- frontend/src/hooks/useMapData.ts | 34 +- frontend/src/hooks/usePaneResize.ts | 23 +- frontend/src/hooks/useTravelTime.ts | 14 +- frontend/src/hooks/useTutorial.ts | 58 +- frontend/src/i18n/descriptions.ts | 165 ++- frontend/src/i18n/details.ts | 530 +++++++++ frontend/src/i18n/index.ts | 62 +- frontend/src/i18n/server.ts | 4 +- frontend/src/lib/PieHexExtension.ts | 124 ++ frontend/src/lib/format.ts | 6 +- frontend/src/lib/map-utils.ts | 2 +- frontend/src/lib/url-state.ts | 6 +- server-rs/logs/server.log.2026-04-04 | 1017 +++++++++++++++++ server-rs/src/aggregation.rs | 46 +- server-rs/src/parsing/fields.rs | 27 + server-rs/src/parsing/filters.rs | 35 +- server-rs/src/routes/ai_filters.rs | 5 +- 21 files changed, 2522 insertions(+), 423 deletions(-) create mode 100644 frontend/src/i18n/details.ts create mode 100644 frontend/src/lib/PieHexExtension.ts create mode 100644 server-rs/logs/server.log.2026-04-04 diff --git a/frontend/src/components/map/LocationSearch.tsx b/frontend/src/components/map/LocationSearch.tsx index f23c404..cd2b176 100644 --- a/frontend/src/components/map/LocationSearch.tsx +++ b/frontend/src/components/map/LocationSearch.tsx @@ -11,6 +11,8 @@ import { SearchIcon } from '../ui/icons/SearchIcon'; export interface SearchedLocation { postcode: string; geometry: PostcodeGeometry; + latitude: number; + longitude: number; } const ZOOM_FOR_TYPE: Record = { @@ -94,7 +96,12 @@ export default function LocationSearch({ geometry: PostcodeGeometry; } = await res.json(); onFlyTo(json.latitude, json.longitude, 16); - onLocationSearched?.({ postcode: json.postcode, geometry: json.geometry }); + onLocationSearched?.({ + postcode: json.postcode, + geometry: json.geometry, + latitude: json.latitude, + longitude: json.longitude, + }); search.clear(); if (isMobile) setExpanded(false); } catch { @@ -139,7 +146,12 @@ export default function LocationSearch({ geometry: PostcodeGeometry; } = await res.json(); onFlyTo(json.latitude, json.longitude, 16); - onLocationSearched?.({ postcode: json.postcode, geometry: json.geometry }); + onLocationSearched?.({ + postcode: json.postcode, + geometry: json.geometry, + latitude: json.latitude, + longitude: json.longitude, + }); search.clear(); if (isMobile) setExpanded(false); } catch { diff --git a/frontend/src/components/map/MapPage.tsx b/frontend/src/components/map/MapPage.tsx index 522de4a..5f1fe5b 100644 --- a/frontend/src/components/map/MapPage.tsx +++ b/frontend/src/components/map/MapPage.tsx @@ -116,7 +116,7 @@ export default function MapPage({ const [leftPaneWidth, leftPaneHandlers] = usePaneResize(384, 200, 0.45, 'left'); const [rightPaneWidth, rightPaneHandlers] = usePaneResize(384, 200, 0.45, 'right'); - const [mobileMapHeight, mobileResizeHandlers, mobileMapRef] = usePaneResize( + const [, mobileResizeHandlers, mobileMapRef] = usePaneResize( Math.round(window.innerHeight * 0.4), 120, 0.8, @@ -167,16 +167,32 @@ export default function MapPage({ features, }); - const aiFilters = useAiFilters(); + const { + fetchAiFilters, + loading: aiFilterLoading, + error: aiFilterError, + errorType: aiFilterErrorType, + notes: aiFilterNotes, + summary: aiFilterSummary, + } = useAiFilters(); - const travelTime = useTravelTime(initialTravelTime); + const { + entries, + activeEntries, + handleAddEntry, + handleRemoveEntry, + handleSetDestination, + handleSetEntries, + handleTimeRangeChange, + handleToggleBest, + } = useTravelTime(initialTravelTime); const handleAiFilterSubmit = useCallback( async (query: string) => { // Build context from current filters for conversational refinement const context = { filters, - travelTime: travelTime.activeEntries.map((entry) => ({ + travelTime: activeEntries.map((entry) => ({ mode: entry.mode, label: entry.label, min: entry.timeRange?.[0], @@ -185,7 +201,7 @@ export default function MapPage({ }; const hasContext = Object.keys(context.filters).length > 0 || context.travelTime.length > 0; - const result = await aiFilters.fetchAiFilters(query, hasContext ? context : undefined); + const result = await fetchAiFilters(query, hasContext ? context : undefined); if (!result) return; handleSetFilters(result.filters); // Always sync travel time entries — clear stale ones when AI returns none @@ -196,40 +212,34 @@ export default function MapPage({ timeRange: [tt.min ?? 0, tt.max ?? 120] as [number, number], useBest: false, })); - travelTime.handleSetEntries(newEntries); + handleSetEntries(newEntries); }, - [ - aiFilters.fetchAiFilters, - handleSetFilters, - travelTime.handleSetEntries, - travelTime.activeEntries, - filters, - ] + [fetchAiFilters, handleSetFilters, handleSetEntries, activeEntries, filters] ); const handleClearAll = useCallback(() => { handleSetFilters({}); handleCancelPin(); - travelTime.handleSetEntries([]); - }, [handleSetFilters, handleCancelPin, travelTime.handleSetEntries]); + handleSetEntries([]); + }, [handleSetFilters, handleCancelPin, handleSetEntries]); const handleTravelTimeRemoveEntry = useCallback( (index: number) => { - const entry = travelTime.entries[index]; + const entry = entries[index]; if (entry?.slug && pinnedFeature === travelFieldKey(entry)) { handleCancelPin(); } - travelTime.handleRemoveEntry(index); + handleRemoveEntry(index); }, - [travelTime.handleRemoveEntry, travelTime.entries, pinnedFeature, handleCancelPin] + [handleRemoveEntry, entries, pinnedFeature, handleCancelPin] ); const handleTravelTimeDragEnd = useCallback( (index: number) => { const dv = handleDragEndNoCommit(); - if (dv) travelTime.handleTimeRangeChange(index, dv); + if (dv) handleTimeRangeChange(index, dv); }, - [handleDragEndNoCommit, travelTime.handleTimeRangeChange] + [handleDragEndNoCommit, handleTimeRangeChange] ); const license = useLicense(); @@ -241,28 +251,46 @@ export default function MapPage({ features, viewFeature, activeFeature, - travelTimeEntries: travelTime.entries, + travelTimeEntries: entries, }); - const filterCounts = useFilterCounts(filters, features, mapData.bounds, travelTime.entries); + const filterCounts = useFilterCounts(filters, features, mapData.bounds, entries); const handleTravelTimeSetDestination = useCallback( (index: number, slug: string, label: string, lat: number, lon: number) => { - travelTime.handleSetDestination(index, slug, label); + handleSetDestination(index, slug, label); if (slug) { mapFlyToRef.current?.(lat, lon, mapData.currentView?.zoom ?? INITIAL_VIEW_STATE.zoom); } }, - [travelTime.handleSetDestination, mapData.currentView?.zoom] + [handleSetDestination, mapData.currentView?.zoom] ); // First transit destination — used to pick the best central_postcode for journey display const journeyDest = useMemo(() => { - const entry = travelTime.entries.find((e) => e.mode === 'transit' && e.slug); + const entry = entries.find((e) => e.mode === 'transit' && e.slug); return entry ? { mode: entry.mode, slug: entry.slug } : null; - }, [travelTime.entries]); + }, [entries]); - const selection = useHexagonSelection({ + const { + selectedHexagon, + properties, + propertiesTotal, + loadingProperties, + areaStats, + loadingAreaStats, + hoveredHexagon, + rightPaneTab, + setRightPaneTab, + handleHexagonClick, + handleHexagonHover, + handleViewPropertiesFromArea, + handlePropertiesTabClick, + handleLoadMoreProperties, + handleCloseSelection, + selectedPostcodeGeometry, + handleLocationSearch, + } = useHexagonSelection({ filters, features, resolution: mapData.resolution, @@ -272,13 +300,13 @@ export default function MapPage({ const handleLocationSearchResult = useCallback( (result: SearchedLocation | null) => { if (result) { - selection.handleLocationSearch(result.postcode, result.geometry); + handleLocationSearch(result.postcode, result.geometry, result.latitude, result.longitude); if (isMobile) setMobileDrawerOpen(true); } else { - selection.handleCloseSelection(); + handleCloseSelection(); } }, - [selection.handleLocationSearch, selection.handleCloseSelection, isMobile] + [handleLocationSearch, handleCloseSelection, isMobile] ); const handleZoomToFreeZone = useCallback(() => { @@ -292,18 +320,11 @@ export default function MapPage({ const pois = usePOIData(mapData.bounds, selectedPOICategories); const [isAreaGroupExpanded, toggleAreaGroup] = useCollapsibleGroups(true); - useUrlSync( - mapData.currentView, - filters, - features, - selectedPOICategories, - selection.rightPaneTab, - travelTime.entries - ); + useUrlSync(mapData.currentView, filters, features, selectedPOICategories, rightPaneTab, entries); useEffect(() => { mapData.setInitialView(initialViewState); - selection.setRightPaneTab(initialTab); + setRightPaneTab(initialTab); }, []); // eslint-disable-line react-hooks/exhaustive-deps // Navigate to a specific postcode on mount (e.g. from saved properties) @@ -329,7 +350,7 @@ export default function MapPage({ geometry: PostcodeGeometry; }) => { mapFlyToRef.current?.(data.latitude, data.longitude, 16); - selection.handleLocationSearch(data.postcode, data.geometry); + handleLocationSearch(data.postcode, data.geometry, data.latitude, data.longitude); if (isMobile) setMobileDrawerOpen(true); } ) @@ -361,7 +382,6 @@ export default function MapPage({ return () => window.removeEventListener('popstate', handlePopState); }, [isMobile]); - const { handleHexagonClick } = selection; const handleMobileHexagonClick = useCallback( (id: string, isPostcode?: boolean, geometry?: PostcodeGeometry) => { handleHexagonClick(id, isPostcode, geometry); @@ -373,8 +393,8 @@ export default function MapPage({ ); const hexagonLocation = useMemo(() => { - const hexId = selection.selectedHexagon?.id; - const isPostcode = selection.selectedHexagon?.type === 'postcode'; + const hexId = selectedHexagon?.id; + const isPostcode = selectedHexagon?.type === 'postcode'; if (isPostcode) { // For postcodes, get centroid from postcodeData; postcode string is the selection id @@ -390,16 +410,16 @@ export default function MapPage({ lat: hex.lat as number, lon: hex.lon as number, resolution: mapData.resolution, - postcode: selection.areaStats?.central_postcode, + postcode: areaStats?.central_postcode, }; } }, [ - selection.selectedHexagon?.id, - selection.selectedHexagon?.type, + selectedHexagon?.id, + selectedHexagon?.type, mapData.data, mapData.postcodeData, mapData.resolution, - selection.areaStats?.central_postcode, + areaStats?.central_postcode, ]); const tutorial = useTutorial(initialLoading, isMobile, deferTutorial); @@ -548,7 +568,7 @@ export default function MapPage({ screenshotMode ogMode={ogMode} bounds={mapData.bounds} - travelTimeEntries={travelTime.entries} + travelTimeEntries={entries} /> ); @@ -556,22 +576,20 @@ export default function MapPage({ const renderAreaPane = () => ( f.properties.postcode === selection.selectedHexagon?.id - ) || null + selectedHexagon?.type === 'postcode' + ? mapData.postcodeData.find((f) => f.properties.postcode === selectedHexagon?.id) || null : null } - onViewProperties={selection.handleViewPropertiesFromArea} + onViewProperties={handleViewPropertiesFromArea} hexagonLocation={hexagonLocation} filters={filters} - travelTimeEntries={travelTime.activeEntries} + travelTimeEntries={activeEntries} isGroupExpanded={isAreaGroupExpanded} onToggleGroup={toggleAreaGroup} /> @@ -579,11 +597,11 @@ export default function MapPage({ const renderPropertiesPane = () => ( {})} @@ -661,7 +679,6 @@ export default function MapPage({
{mapData.loading && (
@@ -773,17 +790,17 @@ export default function MapPage({
{renderFilters()}
- {mobileDrawerOpen && selection.selectedHexagon && ( + {mobileDrawerOpen && selectedHexagon && ( setMobileDrawerOpen(false)} renderArea={renderAreaPane} renderProperties={renderPropertiesPane} - tab={selection.rightPaneTab} + tab={rightPaneTab} onTabChange={(t) => { if (t === 'properties') { - selection.handlePropertiesTabClick(); + handlePropertiesTabClick(); } else { - selection.setRightPaneTab(t); + setRightPaneTab(t); } }} /> @@ -860,18 +877,18 @@ export default function MapPage({ viewSource={viewSource} onCancelPin={handleCancelPin} features={features} - selectedHexagonId={selection.selectedHexagon?.id || null} - hoveredHexagonId={selection.hoveredHexagon} - onHexagonClick={selection.handleHexagonClick} - onHexagonHover={selection.handleHexagonHover} + selectedHexagonId={selectedHexagon?.id || null} + hoveredHexagonId={hoveredHexagon} + onHexagonClick={handleHexagonClick} + onHexagonHover={handleHexagonHover} initialViewState={initialViewState} flyToRef={mapFlyToRef} theme={theme} filters={filters} - selectedPostcodeGeometry={selection.selectedPostcodeGeometry} + selectedPostcodeGeometry={selectedPostcodeGeometry} onLocationSearched={handleLocationSearchResult} bounds={mapData.bounds} - travelTimeEntries={travelTime.entries} + travelTimeEntries={entries} densityLabel={densityLabel} totalCount={filterCounts.total || undefined} /> @@ -902,7 +919,7 @@ export default function MapPage({ )}
- {selection.selectedHexagon && ( + {selectedHexagon && (
selection.setRightPaneTab('area')} + isActive={rightPaneTab === 'area'} + onClick={() => setRightPaneTab('area')} />
- {selection.rightPaneTab === 'properties' ? renderPropertiesPane() : renderAreaPane()} + {rightPaneTab === 'properties' ? renderPropertiesPane() : renderAreaPane()}
diff --git a/frontend/src/hooks/useDeckLayers.ts b/frontend/src/hooks/useDeckLayers.ts index c60a68e..91e4f79 100644 --- a/frontend/src/hooks/useDeckLayers.ts +++ b/frontend/src/hooks/useDeckLayers.ts @@ -1,6 +1,6 @@ import { useCallback, useRef, useState, useMemo, useEffect } from 'react'; import { H3HexagonLayer } from '@deck.gl/geo-layers'; -import { GeoJsonLayer, IconLayer, TextLayer, ScatterplotLayer } from '@deck.gl/layers'; +import { GeoJsonLayer, IconLayer, TextLayer, ScatterplotLayer, PolygonLayer } from '@deck.gl/layers'; import { cellToBoundary } from 'h3-js'; import Supercluster from 'supercluster'; import type { PickingInfo } from '@deck.gl/core'; @@ -26,6 +26,7 @@ import { import { emojiToTwemojiUrl, getFeatureFillColor } from '../lib/map-utils'; import type { TravelTimeEntry } from './useTravelTime'; import { MarchingAntsExtension } from '../lib/MarchingAntsExtension'; +import { PieHexExtension } from '../lib/PieHexExtension'; interface UseDeckLayersProps { data: HexagonData[]; @@ -66,6 +67,17 @@ interface ClusterPoint { clusterId: number; } +/** Normalize a distribution count array to [0..1] ratios, padded to 10 values. */ +function distToRatios(dist: unknown): number[] { + if (!Array.isArray(dist) || dist.length === 0) return [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + let total = 0; + for (let i = 0; i < dist.length; i++) total += (dist[i] as number) || 0; + if (total === 0) return [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + const r = new Array(10).fill(0); + for (let i = 0; i < Math.min(dist.length, 10); i++) r[i] = ((dist[i] as number) || 0) / total; + return r; +} + export function useDeckLayers({ data, postcodeData, @@ -294,215 +306,331 @@ export function useDeckLayers({ const postcodeColorTrigger = `${viewFeature}|${colorRange?.[0]}|${colorRange?.[1]}|${filterRange?.[0]}|${filterRange?.[1]}|${postcodeCountRange.min}|${postcodeCountRange.max}|${hoveredPostcode}|${theme}|${ttTrigger}`; // --- Layers --- - const hexLayer = useMemo( - () => - new H3HexagonLayer({ + // For enum features, we bypass H3HexagonLayer and use PolygonLayer directly. + // H3HexagonLayer has double CompositeLayer nesting (H3 → PolygonLayer → SolidPolygonLayer) + // which prevents custom binary attributes from reaching the fill sublayer. + // PolygonLayer has only one level of nesting, so _subLayerProps.fill works reliably. + const hexLayer = useMemo(() => { + const isEnum = enumCountRef.current > 0; + + if (isEnum) { + const distKey = viewFeatureRef.current ? `dist_${viewFeatureRef.current}` : ''; + const n = data.length; + + // Pre-compute hex boundaries and binary attribute buffers + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const polyData: any[] = new Array(n); + const centers = new Float32Array(n * 2); + const r0 = new Float32Array(n * 4); + const r1 = new Float32Array(n * 4); + const r2 = new Float32Array(n * 2); + for (let i = 0; i < n; i++) { + const d = data[i]; + polyData[i] = { ...d, polygon: cellToBoundary(d.h3, true) }; + centers[i * 2] = d.lon as number; + centers[i * 2 + 1] = d.lat as number; + const r = distToRatios(d[distKey]); + r0[i * 4] = r[0]; + r0[i * 4 + 1] = r[1]; + r0[i * 4 + 2] = r[2]; + r0[i * 4 + 3] = r[3]; + r1[i * 4] = r[4]; + r1[i * 4 + 1] = r[5]; + r1[i * 4 + 2] = r[6]; + r1[i * 4 + 3] = r[7]; + r2[i * 2] = r[8]; + r2[i * 2 + 1] = r[9]; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return new (PolygonLayer as any)({ id: 'h3-hexagons', - data, - getHexagon: (d) => d.h3, - getFillColor: (d) => { - const dark = isDarkRef.current; - const vf = viewFeatureRef.current; - const clr = colorRangeRef.current; - const fr = filterRangeRef.current; - const cfm = colorFeatureMetaRef.current; - - if (vf && clr) { - // Travel time feature: dim hexagons with no data - if (vf.startsWith('tt_')) { - const ttVal = d[`avg_${vf}`]; - if (ttVal == null) { - return (dark ? [80, 70, 65, 80] : [128, 128, 128, 80]) as [ - number, - number, - number, - number, - ]; - } - const ttMin = (d[`min_${vf}`] as number) ?? ttVal; - const ttMax = (d[`max_${vf}`] as number) ?? ttVal; - return getFeatureFillColor( - ttVal as number, - ttMin as number, - ttMax as number, - clr, - fr, - 0, - densityGradientRef.current, - dark, - 255 - ); - } - - // Regular feature - if (cfm) { - const val = d[`avg_${vf}`] ?? d[`min_${vf}`]; - const minVal = d[`min_${vf}`] as number | undefined; - const maxVal = d[`max_${vf}`] as number | undefined; - return getFeatureFillColor( - val as number | null | undefined, - minVal, - maxVal, - clr, - fr, - 0, - densityGradientRef.current, - dark, - 255, - enumCountRef.current - ); - } - } - - // Density fallback - const cr = countRangeRef.current; - const c = d.count as number; - const t = (c - cr.min) / (cr.max - cr.min); - return getFeatureFillColor( - null, - undefined, - undefined, - null, - null, - t, - densityGradientRef.current, - dark, - 255 - ); - }, - getLineColor: (d) => { + data: polyData, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getPolygon: (d: any) => d.polygon, + getFillColor: [200, 200, 200], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getLineColor: (d: any) => { if (d.h3 === hoveredHexagonIdRef.current) - return [29, 228, 195, 200] as [number, number, number, number]; - return [0, 0, 0, 0] as [number, number, number, number]; + return [29, 228, 195, 200]; + return [0, 0, 0, 0]; }, - getLineWidth: (d) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getLineWidth: (d: any) => { if (d.h3 === hoveredHexagonIdRef.current) return 2; return 0; }, lineWidthUnits: 'pixels', updateTriggers: { - getFillColor: [colorTrigger], getLineColor: [colorTrigger], getLineWidth: [colorTrigger], }, + extensions: [new PieHexExtension()], + _subLayerProps: { + fill: { + instancePieCenter: { value: centers, size: 2 }, + instanceRatios0: { value: r0, size: 4 }, + instanceRatios1: { value: r1, size: 4 }, + instanceRatios2: { value: r2, size: 2 }, + }, + }, extruded: false, pickable: true, - opacity: 1, - highPrecision: true, onClick: handleHexagonClick, onHover: handleHexagonHover, - // @ts-expect-error beforeId is a MapboxOverlay interleave prop, not typed in LayerProps beforeId: 'landuse_park', - }), - [data, colorTrigger, handleHexagonClick, handleHexagonHover] - ); + }); + } - const postcodeLayer = useMemo( - () => - new GeoJsonLayer({ - id: 'postcode-polygons', - data: postcodeData as PostcodeFeature[], - getFillColor: (f) => { - const d = f.properties; - const dark = isDarkRef.current; - const vf = viewFeatureRef.current; - const clr = colorRangeRef.current; - const fr = filterRangeRef.current; - const cfm = colorFeatureMetaRef.current; + // Non-enum: use H3HexagonLayer as normal + return new H3HexagonLayer({ + id: 'h3-hexagons', + data, + getHexagon: (d) => d.h3, + getFillColor: (d) => { + const dark = isDarkRef.current; + const vf = viewFeatureRef.current; + const clr = colorRangeRef.current; + const fr = filterRangeRef.current; + const cfm = colorFeatureMetaRef.current; - if (vf && clr) { - // Travel time feature: dim postcodes with no data - if (vf.startsWith('tt_')) { - const ttVal = d[`avg_${vf}`]; - if (ttVal == null) { - return (dark ? [80, 70, 65, 80] : [128, 128, 128, 80]) as [ - number, - number, - number, - number, - ]; - } - const ttMin = (d[`min_${vf}`] as number) ?? ttVal; - const ttMax = (d[`max_${vf}`] as number) ?? ttVal; - return getFeatureFillColor( - ttVal as number, - ttMin as number, - ttMax as number, - clr, - fr, - 0, - densityGradientRef.current, - dark, - 180 - ); - } - - // Regular feature - if (cfm) { - const val = d[`avg_${vf}`] ?? d[`min_${vf}`]; - const minVal = d[`min_${vf}`] as number | undefined; - const maxVal = d[`max_${vf}`] as number | undefined; - return getFeatureFillColor( - val as number | null | undefined, - minVal, - maxVal, - clr, - fr, - 0, - densityGradientRef.current, - dark, - 180, - enumCountRef.current - ); + if (vf && clr) { + if (vf.startsWith('tt_')) { + const ttVal = d[`avg_${vf}`]; + if (ttVal == null) { + return (dark ? [80, 70, 65, 80] : [128, 128, 128, 80]) as [ + number, + number, + number, + number, + ]; } + const ttMin = (d[`min_${vf}`] as number) ?? ttVal; + const ttMax = (d[`max_${vf}`] as number) ?? ttVal; + return getFeatureFillColor( + ttVal as number, + ttMin as number, + ttMax as number, + clr, + fr, + 0, + densityGradientRef.current, + dark, + 255 + ); } - const cr = postcodeCountRangeRef.current; - const c = d.count; - const t = (c - cr.min) / (cr.max - cr.min); - return getFeatureFillColor( - null, - undefined, - undefined, - null, - null, - t, - densityGradientRef.current, - dark, - 180 - ); + + if (cfm) { + const val = d[`avg_${vf}`] ?? d[`min_${vf}`]; + const minVal = d[`min_${vf}`] as number | undefined; + const maxVal = d[`max_${vf}`] as number | undefined; + return getFeatureFillColor( + val as number | null | undefined, + minVal, + maxVal, + clr, + fr, + 0, + densityGradientRef.current, + dark, + 255, + enumCountRef.current + ); + } + } + + const cr = countRangeRef.current; + const c = d.count as number; + const t = (c - cr.min) / (cr.max - cr.min); + return getFeatureFillColor( + null, + undefined, + undefined, + null, + null, + t, + densityGradientRef.current, + dark, + 255 + ); + }, + getLineColor: (d) => { + if (d.h3 === hoveredHexagonIdRef.current) + return [29, 228, 195, 200] as [number, number, number, number]; + return [0, 0, 0, 0] as [number, number, number, number]; + }, + getLineWidth: (d) => { + if (d.h3 === hoveredHexagonIdRef.current) return 2; + return 0; + }, + lineWidthUnits: 'pixels', + updateTriggers: { + getFillColor: [colorTrigger], + getLineColor: [colorTrigger], + getLineWidth: [colorTrigger], + }, + extruded: false, + pickable: true, + opacity: 1, + highPrecision: true, + onClick: handleHexagonClick, + onHover: handleHexagonHover, + // @ts-expect-error beforeId is a MapboxOverlay interleave prop, not typed in LayerProps + beforeId: 'landuse_park', + }); + }, [data, colorTrigger, handleHexagonClick, handleHexagonHover]); + + const postcodeLayer = useMemo(() => { + const isEnum = enumCountRef.current > 0; + const distKey = viewFeatureRef.current ? `dist_${viewFeatureRef.current}` : ''; + + // Same binary buffer approach as hexagons, routed via _subLayerProps. + // GeoJsonLayer → 'polygons-fill' (PolygonLayer) → 'fill' (SolidPolygonLayer) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let pieProps: any = {}; + if (isEnum) { + const n = postcodeData.length; + const centers = new Float32Array(n * 2); + const r0 = new Float32Array(n * 4); + const r1 = new Float32Array(n * 4); + const r2 = new Float32Array(n * 2); + for (let i = 0; i < n; i++) { + const centroid = postcodeData[i].properties.centroid as [number, number]; + centers[i * 2] = centroid[0]; + centers[i * 2 + 1] = centroid[1]; + const r = distToRatios(postcodeData[i].properties[distKey]); + r0[i * 4] = r[0]; + r0[i * 4 + 1] = r[1]; + r0[i * 4 + 2] = r[2]; + r0[i * 4 + 3] = r[3]; + r1[i * 4] = r[4]; + r1[i * 4 + 1] = r[5]; + r1[i * 4 + 2] = r[6]; + r1[i * 4 + 3] = r[7]; + r2[i * 2] = r[8]; + r2[i * 2 + 1] = r[9]; + } + const fillAttrs = { + instancePieCenter: { value: centers, size: 2 }, + instanceRatios0: { value: r0, size: 4 }, + instanceRatios1: { value: r1, size: 4 }, + instanceRatios2: { value: r2, size: 2 }, + }; + pieProps = { + extensions: [new PieHexExtension()], + _subLayerProps: { + 'polygons-fill': { + _subLayerProps: { + fill: fillAttrs, + }, + }, }, - getLineColor: (f) => { - const pc = f.properties.postcode; - const dark = isDarkRef.current; - if (pc === hoveredPostcodeRef.current) - return [29, 228, 195, 200] as [number, number, number, number]; - return (dark ? [180, 170, 160, 100] : [100, 100, 100, 150]) as [ - number, - number, - number, - number, - ]; - }, - getLineWidth: (f) => { - const pc = f.properties.postcode; - if (pc === hoveredPostcodeRef.current) return 2; - return 1; - }, - lineWidthUnits: 'pixels', - updateTriggers: { - getFillColor: [postcodeColorTrigger], - getLineColor: [postcodeColorTrigger], - getLineWidth: [postcodeColorTrigger], - }, - extruded: false, - pickable: true, - onClick: handlePostcodeClick, - onHover: handlePostcodeHoverCallback, - // @ts-expect-error beforeId is a MapboxOverlay interleave prop, not typed in LayerProps - beforeId: 'landuse_park', - }), - [postcodeData, postcodeColorTrigger, handlePostcodeClick, handlePostcodeHoverCallback] - ); + }; + } + + return new GeoJsonLayer({ + id: 'postcode-polygons', + data: postcodeData as PostcodeFeature[], + getFillColor: (f) => { + const d = f.properties; + const dark = isDarkRef.current; + const vf = viewFeatureRef.current; + const clr = colorRangeRef.current; + const fr = filterRangeRef.current; + const cfm = colorFeatureMetaRef.current; + + if (vf && clr) { + // Travel time feature: dim postcodes with no data + if (vf.startsWith('tt_')) { + const ttVal = d[`avg_${vf}`]; + if (ttVal == null) { + return (dark ? [80, 70, 65, 80] : [128, 128, 128, 80]) as [ + number, + number, + number, + number, + ]; + } + const ttMin = (d[`min_${vf}`] as number) ?? ttVal; + const ttMax = (d[`max_${vf}`] as number) ?? ttVal; + return getFeatureFillColor( + ttVal as number, + ttMin as number, + ttMax as number, + clr, + fr, + 0, + densityGradientRef.current, + dark, + 180 + ); + } + + // Regular feature (for enum, the extension overrides this color) + if (cfm) { + const val = d[`avg_${vf}`] ?? d[`min_${vf}`]; + const minVal = d[`min_${vf}`] as number | undefined; + const maxVal = d[`max_${vf}`] as number | undefined; + return getFeatureFillColor( + val as number | null | undefined, + minVal, + maxVal, + clr, + fr, + 0, + densityGradientRef.current, + dark, + 180, + enumCountRef.current + ); + } + } + const cr = postcodeCountRangeRef.current; + const c = d.count; + const t = (c - cr.min) / (cr.max - cr.min); + return getFeatureFillColor( + null, + undefined, + undefined, + null, + null, + t, + densityGradientRef.current, + dark, + 180 + ); + }, + getLineColor: (f) => { + const pc = f.properties.postcode; + const dark = isDarkRef.current; + if (pc === hoveredPostcodeRef.current) + return [29, 228, 195, 200] as [number, number, number, number]; + return (dark ? [180, 170, 160, 100] : [100, 100, 100, 150]) as [ + number, + number, + number, + number, + ]; + }, + getLineWidth: (f) => { + const pc = f.properties.postcode; + if (pc === hoveredPostcodeRef.current) return 2; + return 1; + }, + lineWidthUnits: 'pixels', + updateTriggers: { + getFillColor: [postcodeColorTrigger], + getLineColor: [postcodeColorTrigger], + getLineWidth: [postcodeColorTrigger], + }, + extruded: false, + pickable: true, + onClick: handlePostcodeClick, + onHover: handlePostcodeHoverCallback, + beforeId: 'landuse_park', + ...pieProps, + }); + }, [postcodeData, postcodeColorTrigger, handlePostcodeClick, handlePostcodeHoverCallback]); const postcodeLabelsLayer = useMemo( () => diff --git a/frontend/src/hooks/useHexagonSelection.ts b/frontend/src/hooks/useHexagonSelection.ts index 8254c9c..2a5118c 100644 --- a/frontend/src/hooks/useHexagonSelection.ts +++ b/frontend/src/hooks/useHexagonSelection.ts @@ -1,4 +1,5 @@ import { useState, useCallback, useEffect, useMemo, useRef } from 'react'; +import { latLngToCell } from 'h3-js'; import { trackEvent } from '../lib/analytics'; import type { FeatureMeta, @@ -287,25 +288,68 @@ export function useHexagonSelection({ return () => { cancelled = true; }; - }, [filterStr, selectedHexagon, fetchHexagonStats, fetchPostcodeStats, rightPaneTab, fetchHexagonProperties, fetchPostcodeProperties]); + }, [ + filterStr, + selectedHexagon, + fetchHexagonStats, + fetchPostcodeStats, + rightPaneTab, + fetchHexagonProperties, + fetchPostcodeProperties, + ]); const handleLocationSearch = useCallback( - (postcode: string, geometry: PostcodeGeometry) => { + (postcode: string, geometry: PostcodeGeometry, lat?: number, lng?: number) => { trackEvent('Postcode Search'); - setSelectedHexagon({ id: postcode, type: 'postcode', resolution }); - setSelectedPostcodeGeometry(geometry); setProperties([]); setPropertiesTotal(0); setPropertiesOffset(0); setRightPaneTab('area'); - setLoadingAreaStats(true); + + // First try the postcode; if it has no properties, fall back to hexagons fetchPostcodeStats(postcode) - .then((stats) => setAreaStats(stats)) + .then(async (stats) => { + if (stats.count > 0) { + setSelectedHexagon({ id: postcode, type: 'postcode', resolution }); + setSelectedPostcodeGeometry(geometry); + setAreaStats(stats); + return; + } + + // No properties in this postcode — fall back to hexagons + if (lat == null || lng == null) { + // No coordinates available, show empty postcode anyway + setSelectedHexagon({ id: postcode, type: 'postcode', resolution }); + setSelectedPostcodeGeometry(geometry); + setAreaStats(stats); + return; + } + + // Try progressively coarser H3 resolutions until we find >1 property + const resolutions = [9, 8, 7, 6, 5]; + for (const res of resolutions) { + const h3 = latLngToCell(lat, lng, res); + const hexStats = await fetchHexagonStats(h3, res); + if (hexStats.count > 1) { + setSelectedHexagon({ id: h3, type: 'hexagon', resolution: res }); + setSelectedPostcodeGeometry(null); + setAreaStats(hexStats); + return; + } + } + + // Even the coarsest hexagon has ≤1 property — show whatever the finest has + const h3 = latLngToCell(lat, lng, 9); + const fallbackStats = await fetchHexagonStats(h3, 9); + setSelectedHexagon({ id: h3, type: 'hexagon', resolution: 9 }); + setSelectedPostcodeGeometry(null); + setAreaStats(fallbackStats); + }) .catch((error) => logNonAbortError('Failed to fetch postcode stats', error)) .finally(() => setLoadingAreaStats(false)); }, - [resolution, fetchPostcodeStats] + [resolution, fetchPostcodeStats, fetchHexagonStats] ); return { diff --git a/frontend/src/hooks/useMapData.ts b/frontend/src/hooks/useMapData.ts index 389d848..b6c54ba 100644 --- a/frontend/src/hooks/useMapData.ts +++ b/frontend/src/hooks/useMapData.ts @@ -75,6 +75,12 @@ export function useMapData({ const usePostcodeView = zoom >= POSTCODE_ZOOM_THRESHOLD; + // Determine if the current viewFeature is an enum (for enum_dist param) + const viewFeatureIsEnum = useMemo( + () => (viewFeature ? features.find((f) => f.name === viewFeature)?.type === 'enum' : false), + [viewFeature, features] + ); + const buildFilterParam = useCallback( (): string => buildFilterString(filters, features), [filters, features] @@ -134,6 +140,7 @@ export function useMapData({ if (filtersStr) params.set('filters', filtersStr); params.set('fields', fieldsParam); if (dragTravelParam) params.set('travel', dragTravelParam); + if (viewFeatureIsEnum && viewFeature) params.set('enum_dist', viewFeature); fetch(apiUrl('postcodes', params), authHeaders({ signal: dragAbortRef.current.signal })) .then((res) => res.json()) @@ -151,6 +158,7 @@ export function useMapData({ if (filtersStr) params.set('filters', filtersStr); params.set('fields', fieldsParam); if (dragTravelParam) params.set('travel', dragTravelParam); + if (viewFeatureIsEnum && viewFeature) params.set('enum_dist', viewFeature); fetch(apiUrl('hexagons', params), authHeaders({ signal: dragAbortRef.current.signal })) .then((res) => res.json()) @@ -168,7 +176,18 @@ export function useMapData({ dragAbortRef.current = null; } }; - }, [activeFeature, bounds, resolution, filters, features, usePostcodeView, travelParam, buildTravelParam]); + }, [ + activeFeature, + bounds, + resolution, + filters, + features, + usePostcodeView, + travelParam, + buildTravelParam, + viewFeature, + viewFeatureIsEnum, + ]); // Fetch hexagons or postcodes when bounds/filters change useEffect(() => { @@ -196,6 +215,7 @@ export function useMapData({ if (travelParam) { params.set('travel', travelParam); } + if (viewFeatureIsEnum && viewFeature) params.set('enum_dist', viewFeature); const res = await fetch( apiUrl('postcodes', params), authHeaders({ @@ -226,6 +246,7 @@ export function useMapData({ if (travelParam) { params.set('travel', travelParam); } + if (viewFeatureIsEnum && viewFeature) params.set('enum_dist', viewFeature); const res = await fetch( apiUrl('hexagons', params), authHeaders({ @@ -268,7 +289,16 @@ export function useMapData({ clearTimeout(debounceRef.current); } }; - }, [resolution, bounds, filters, buildFilterParam, viewFeature, usePostcodeView, travelParam]); + }, [ + resolution, + bounds, + filters, + buildFilterParam, + viewFeature, + viewFeatureIsEnum, + usePostcodeView, + travelParam, + ]); // Use drag data when it matches the current view feature, otherwise fall back to rawData const data = diff --git a/frontend/src/hooks/usePaneResize.ts b/frontend/src/hooks/usePaneResize.ts index c306a07..d01da2b 100644 --- a/frontend/src/hooks/usePaneResize.ts +++ b/frontend/src/hooks/usePaneResize.ts @@ -1,4 +1,4 @@ -import { useState, useCallback, useRef } from 'react'; +import { useState, useCallback, useRef, useLayoutEffect } from 'react'; interface PaneResizeHandlers { onPointerDown: (e: React.PointerEvent) => void; @@ -22,9 +22,24 @@ export function usePaneResize( const isVertical = side === 'top' || side === 'bottom'; const styleProp = isVertical ? 'height' : 'width'; - const targetCallbackRef = useCallback((el: HTMLElement | null) => { - targetRef.current = el; - }, []); + const targetCallbackRef = useCallback( + (el: HTMLElement | null) => { + targetRef.current = el; + if (el) { + el.style[styleProp] = `${liveSizeRef.current}px`; + } + }, + [styleProp] + ); + + // Keep DOM in sync when React state commits (e.g. on pointerUp). + // This ensures the ref-managed element always reflects the latest size + // without relying on React-controlled style props. + useLayoutEffect(() => { + if (targetRef.current) { + targetRef.current.style[styleProp] = `${size}px`; + } + }, [size, styleProp]); const computeSize = useCallback( (e: React.PointerEvent): number => { diff --git a/frontend/src/hooks/useTravelTime.ts b/frontend/src/hooks/useTravelTime.ts index 36ac643..6dc1ae9 100644 --- a/frontend/src/hooks/useTravelTime.ts +++ b/frontend/src/hooks/useTravelTime.ts @@ -35,12 +35,22 @@ export function useTranslatedModes() { const { t } = useTranslation(); const label = useCallback( (mode: TransportMode): string => - ({ car: t('travel.modeCar'), bicycle: t('travel.modeBicycle'), walking: t('travel.modeWalking'), transit: t('travel.modeTransit') })[mode], + ({ + car: t('travel.modeCar'), + bicycle: t('travel.modeBicycle'), + walking: t('travel.modeWalking'), + transit: t('travel.modeTransit'), + })[mode], [t] ); const desc = useCallback( (mode: TransportMode): string => - ({ car: t('travel.modeCarDesc'), bicycle: t('travel.modeBicycleDesc'), walking: t('travel.modeWalkingDesc'), transit: t('travel.modeTransitDesc') })[mode], + ({ + car: t('travel.modeCarDesc'), + bicycle: t('travel.modeBicycleDesc'), + walking: t('travel.modeWalkingDesc'), + transit: t('travel.modeTransitDesc'), + })[mode], [t] ); return { label, desc }; diff --git a/frontend/src/hooks/useTutorial.ts b/frontend/src/hooks/useTutorial.ts index b01c64c..0ca1172 100644 --- a/frontend/src/hooks/useTutorial.ts +++ b/frontend/src/hooks/useTutorial.ts @@ -8,14 +8,54 @@ const STORAGE_KEY = 'tutorial_completed'; export function useTutorial(initialLoading: boolean, isMobile: boolean, blocked = false) { const { t } = useTranslation(); - const steps: Step[] = useMemo(() => [ - { target: '[data-tutorial="filters"]', title: t('tutorial.step1Title'), content: t('tutorial.step1Content'), placement: 'right' as const, disableBeacon: true }, - { target: '[data-tutorial="ai-filters"]', title: t('tutorial.step2Title'), content: t('tutorial.step2Content'), placement: 'right' as const, disableBeacon: true }, - { target: '[data-tutorial="map"]', title: t('tutorial.step3Title'), content: t('tutorial.step3Content'), placement: 'bottom' as const, disableBeacon: true }, - { target: '[data-tutorial="search"]', title: t('tutorial.step4Title'), content: t('tutorial.step4Content'), placement: 'bottom' as const, disableBeacon: true }, - { target: '[data-tutorial="right-pane"]', title: t('tutorial.step5Title'), content: t('tutorial.step5Content'), placement: 'left' as const, disableBeacon: true }, - { target: '[data-tutorial="poi-button"]', title: t('tutorial.step6Title'), content: t('tutorial.step6Content'), placement: 'left' as const, disableBeacon: true, styles: { tooltip: { transform: 'translateY(-50px)' } } }, - ], [t]); + const steps: Step[] = useMemo( + () => [ + { + target: '[data-tutorial="filters"]', + title: t('tutorial.step1Title'), + content: t('tutorial.step1Content'), + placement: 'right' as const, + disableBeacon: true, + }, + { + target: '[data-tutorial="ai-filters"]', + title: t('tutorial.step2Title'), + content: t('tutorial.step2Content'), + placement: 'right' as const, + disableBeacon: true, + }, + { + target: '[data-tutorial="map"]', + title: t('tutorial.step3Title'), + content: t('tutorial.step3Content'), + placement: 'bottom' as const, + disableBeacon: true, + }, + { + target: '[data-tutorial="search"]', + title: t('tutorial.step4Title'), + content: t('tutorial.step4Content'), + placement: 'bottom' as const, + disableBeacon: true, + }, + { + target: '[data-tutorial="right-pane"]', + title: t('tutorial.step5Title'), + content: t('tutorial.step5Content'), + placement: 'left' as const, + disableBeacon: true, + }, + { + target: '[data-tutorial="poi-button"]', + title: t('tutorial.step6Title'), + content: t('tutorial.step6Content'), + placement: 'left' as const, + disableBeacon: true, + styles: { tooltip: { transform: 'translateY(-50px)' } }, + }, + ], + [t] + ); const [run, setRun] = useState(() => { if (isMobile) return false; @@ -50,6 +90,6 @@ export function useTutorial(initialLoading: boolean, isMobile: boolean, blocked handleCallback, resetTutorial, }), - [shouldRun, handleCallback, resetTutorial] + [steps, shouldRun, handleCallback, resetTutorial] ); } diff --git a/frontend/src/i18n/descriptions.ts b/frontend/src/i18n/descriptions.ts index f9d7387..b58b886 100644 --- a/frontend/src/i18n/descriptions.ts +++ b/frontend/src/i18n/descriptions.ts @@ -12,7 +12,8 @@ import i18n from 'i18next'; */ const descriptions: Record> = { fr: { - 'Listing status': 'Indique si le bien provient de ventes historiques, est en vente ou en location', + 'Listing status': + 'Indique si le bien provient de ventes historiques, est en vente ou en location', 'Property type': 'Type de bien : individuel, jumelé, mitoyen, appartement ou autre', 'Leasehold/Freehold': 'Indique si le bien est en bail ou en pleine propriété', 'Last known price': 'Dernier prix de vente enregistré au Land Registry', @@ -25,43 +26,56 @@ const descriptions: Record> = { 'Asking rent (monthly)': 'Loyer mensuel affiché pour les biens en location', 'Total floor area (sqm)': 'Surface intérieure issue du diagnostic EPC', 'Number of bedrooms & living rooms': 'Nombre de pièces habitables selon le diagnostic EPC', - 'Bedrooms': 'Nombre de chambres selon l’annonce en ligne', - 'Bathrooms': 'Nombre de salles de bain selon l’annonce en ligne', + Bedrooms: 'Nombre de chambres selon l’annonce en ligne', + Bathrooms: 'Nombre de salles de bain selon l’annonce en ligne', 'Construction year': 'Année de construction estimée selon l’EPC', 'Date of last transaction': 'Date de la dernière vente enregistrée au Land Registry', 'Listing date': 'Date de première mise en ligne du bien', 'Former council house': 'Indique si le bien a été répertorié comme logement social', 'Current energy rating': 'Classement énergétique EPC actuel (A = meilleur, G = pire)', - 'Potential energy rating': 'Classement EPC potentiel si toutes les améliorations recommandées étaient réalisées', + 'Potential energy rating': + 'Classement EPC potentiel si toutes les améliorations recommandées étaient réalisées', 'Interior height (m)': 'Hauteur moyenne d’étage selon le diagnostic EPC', - 'Distance to nearest train or tube station (km)': 'Distance à la gare ou station de métro la plus proche', - 'Good+ primary schools within 2km': 'Écoles primaires notées Bien ou Excellent par Ofsted dans un rayon de 2 km', - 'Good+ secondary schools within 2km': 'Collèges/lycées notés Bien ou Excellent par Ofsted dans un rayon de 2 km', - 'Good+ primary schools within 5km': 'Écoles primaires notées Bien ou Excellent par Ofsted dans un rayon de 5 km', - 'Good+ secondary schools within 5km': 'Collèges/lycées notés Bien ou Excellent par Ofsted dans un rayon de 5 km', - 'Education, Skills and Training Score': 'Score de qualité éducative du secteur (plus élevé = meilleur)', + 'Distance to nearest train or tube station (km)': + 'Distance à la gare ou station de métro la plus proche', + 'Good+ primary schools within 2km': + 'Écoles primaires notées Bien ou Excellent par Ofsted dans un rayon de 2 km', + 'Good+ secondary schools within 2km': + 'Collèges/lycées notés Bien ou Excellent par Ofsted dans un rayon de 2 km', + 'Good+ primary schools within 5km': + 'Écoles primaires notées Bien ou Excellent par Ofsted dans un rayon de 5 km', + 'Good+ secondary schools within 5km': + 'Collèges/lycées notés Bien ou Excellent par Ofsted dans un rayon de 5 km', + 'Education, Skills and Training Score': + 'Score de qualité éducative du secteur (plus élevé = meilleur)', 'Income Score (rate)': 'Taux de précarité de revenu, inversé (plus élevé = moins précaire)', 'Employment Score (rate)': 'Taux de précarité d’emploi, inversé (plus élevé = moins précaire)', - 'Health Deprivation and Disability Score': 'Score de santé et handicap (plus élevé = meilleurs résultats)', - 'Living Environment Score': 'Qualité de l’environnement intérieur et extérieur (plus élevé = meilleur)', + 'Health Deprivation and Disability Score': + 'Score de santé et handicap (plus élevé = meilleurs résultats)', + 'Living Environment Score': + 'Qualité de l’environnement intérieur et extérieur (plus élevé = meilleur)', 'Indoors Sub-domain Score': 'Qualité et état du logement (plus élevé = meilleur)', 'Outdoors Sub-domain Score': 'Qualité de l’air et sécurité routière (plus élevé = meilleur)', 'Serious crime per 1k residents (avg/yr)': 'Taux de crimes graves pour 1 000 habitants par an', 'Minor crime per 1k residents (avg/yr)': 'Taux de délits mineurs pour 1 000 habitants par an', 'Serious crime (avg/yr)': 'Agrégat des catégories de crimes graves par an', 'Minor crime (avg/yr)': 'Agrégat des catégories de délits mineurs par an', - 'Violence and sexual offences (avg/yr)': 'Moyenne annuelle des violences et infractions sexuelles dans le secteur', + 'Violence and sexual offences (avg/yr)': + 'Moyenne annuelle des violences et infractions sexuelles dans le secteur', 'Burglary (avg/yr)': 'Moyenne annuelle des cambriolages dans le secteur', 'Robbery (avg/yr)': 'Moyenne annuelle des vols avec violence dans le secteur', 'Vehicle crime (avg/yr)': 'Moyenne annuelle des crimes liés aux véhicules dans le secteur', - 'Anti-social behaviour (avg/yr)': 'Moyenne annuelle des comportements antisociaux dans le secteur', - 'Criminal damage and arson (avg/yr)': 'Moyenne annuelle des dégradations et incendies criminels dans le secteur', + 'Anti-social behaviour (avg/yr)': + 'Moyenne annuelle des comportements antisociaux dans le secteur', + 'Criminal damage and arson (avg/yr)': + 'Moyenne annuelle des dégradations et incendies criminels dans le secteur', 'Other theft (avg/yr)': 'Moyenne annuelle des autres vols dans le secteur', 'Theft from the person (avg/yr)': 'Moyenne annuelle des vols à la personne dans le secteur', 'Shoplifting (avg/yr)': 'Moyenne annuelle des vols à l’étalage dans le secteur', 'Bicycle theft (avg/yr)': 'Moyenne annuelle des vols de vélos dans le secteur', 'Drugs (avg/yr)': 'Moyenne annuelle des infractions liées aux stupéfiants dans le secteur', - 'Possession of weapons (avg/yr)': 'Moyenne annuelle des infractions de possession d’armes dans le secteur', + 'Possession of weapons (avg/yr)': + 'Moyenne annuelle des infractions de possession d’armes dans le secteur', 'Public order (avg/yr)': 'Moyenne annuelle des troubles à l’ordre public dans le secteur', 'Other crime (avg/yr)': 'Moyenne annuelle des autres crimes dans le secteur', 'Median age': 'Âge médian de la population locale', @@ -69,18 +83,22 @@ const descriptions: Record> = { '% South Asian': 'Pourcentage de la population se déclarant Sud-Asiatique', '% Black': 'Pourcentage de la population se déclarant Noire', '% East Asian': 'Pourcentage de la population se déclarant Est-Asiatique', - '% Mixed': 'Pourcentage de la population se déclarant Métisse ou de plusieurs groupes ethniques', + '% Mixed': + 'Pourcentage de la population se déclarant Métisse ou de plusieurs groupes ethniques', '% Other': 'Pourcentage de la population se déclarant d’un autre groupe ethnique', 'Distance to nearest park (km)': 'Distance au parc ou espace vert le plus proche', 'Number of parks within 2km': 'Nombre de parcs et espaces verts à moins de 2 km', 'Number of restaurants within 2km': 'Nombre de restaurants et cafés à moins de 2 km', - 'Number of grocery shops and supermarkets within 2km': 'Nombre d’épiceries et supermarchés à moins de 2 km', + 'Number of grocery shops and supermarkets within 2km': + 'Nombre d’épiceries et supermarchés à moins de 2 km', 'Noise (dB)': 'Niveau de bruit routier au code postal en décibels (Lden)', 'Max available download speed (Mbps)': 'Débit descendant maximal disponible au code postal', }, de: { - 'Listing status': 'Ob die Immobilie aus historischen Verkäufen stammt, aktuell zum Verkauf oder zur Miete steht', - 'Property type': 'Immobilientyp: freistehend, Doppelhaushälfte, Reihenhaus, Wohnung oder sonstige', + 'Listing status': + 'Ob die Immobilie aus historischen Verkäufen stammt, aktuell zum Verkauf oder zur Miete steht', + 'Property type': + 'Immobilientyp: freistehend, Doppelhaushälfte, Reihenhaus, Wohnung oder sonstige', 'Leasehold/Freehold': 'Ob die Immobilie Erbbaurecht oder Volleigentum ist', 'Last known price': 'Letzter Verkaufspreis laut Land Registry', 'Estimated current price': 'Inflationsbereinigter Schätzwert der Immobilie', @@ -92,8 +110,8 @@ const descriptions: Record> = { 'Asking rent (monthly)': 'Angebotene Monatsmiete für Mietimmobilien', 'Total floor area (sqm)': 'Wohnfläche laut EPC-Gutachten', 'Number of bedrooms & living rooms': 'Anzahl bewohnbarer Räume laut EPC-Gutachten', - 'Bedrooms': 'Anzahl Schlafzimmer laut Online-Inserat', - 'Bathrooms': 'Anzahl Badezimmer laut Online-Inserat', + Bedrooms: 'Anzahl Schlafzimmer laut Online-Inserat', + Bathrooms: 'Anzahl Badezimmer laut Online-Inserat', 'Construction year': 'Geschätztes Baujahr laut EPC', 'Date of last transaction': 'Datum des letzten Verkaufs laut Land Registry', 'Listing date': 'Datum der Erstveröffentlichung des Inserats', @@ -101,49 +119,67 @@ const descriptions: Record> = { 'Current energy rating': 'Aktuelle EPC-Energieeffizienzklasse (A = beste, G = schlechteste)', 'Potential energy rating': 'Potenzielle EPC-Klasse bei Umsetzung aller empfohlenen Maßnahmen', 'Interior height (m)': 'Durchschnittliche Geschosshöhe laut EPC-Gutachten', - 'Distance to nearest train or tube station (km)': 'Entfernung zum nächsten Bahn- oder U-Bahnhof', - 'Good+ primary schools within 2km': 'Von Ofsted mit Gut oder Hervorragend bewertete Grundschulen im Umkreis von 2 km', - 'Good+ secondary schools within 2km': 'Von Ofsted mit Gut oder Hervorragend bewertete weiterführende Schulen im Umkreis von 2 km', - 'Good+ primary schools within 5km': 'Von Ofsted mit Gut oder Hervorragend bewertete Grundschulen im Umkreis von 5 km', - 'Good+ secondary schools within 5km': 'Von Ofsted mit Gut oder Hervorragend bewertete weiterführende Schulen im Umkreis von 5 km', + 'Distance to nearest train or tube station (km)': + 'Entfernung zum nächsten Bahn- oder U-Bahnhof', + 'Good+ primary schools within 2km': + 'Von Ofsted mit Gut oder Hervorragend bewertete Grundschulen im Umkreis von 2 km', + 'Good+ secondary schools within 2km': + 'Von Ofsted mit Gut oder Hervorragend bewertete weiterführende Schulen im Umkreis von 2 km', + 'Good+ primary schools within 5km': + 'Von Ofsted mit Gut oder Hervorragend bewertete Grundschulen im Umkreis von 5 km', + 'Good+ secondary schools within 5km': + 'Von Ofsted mit Gut oder Hervorragend bewertete weiterführende Schulen im Umkreis von 5 km', 'Education, Skills and Training Score': 'Bildungsqualitätsscore der Gegend (höher = besser)', - 'Income Score (rate)': 'Einkommensbenachteiligungsrate, invertiert (höher = weniger benachteiligt)', - 'Employment Score (rate)': 'Beschäftigungsbenachteiligungsrate, invertiert (höher = weniger benachteiligt)', - 'Health Deprivation and Disability Score': 'Gesundheits- und Behinderungsscore (höher = bessere Ergebnisse)', + 'Income Score (rate)': + 'Einkommensbenachteiligungsrate, invertiert (höher = weniger benachteiligt)', + 'Employment Score (rate)': + 'Beschäftigungsbenachteiligungsrate, invertiert (höher = weniger benachteiligt)', + 'Health Deprivation and Disability Score': + 'Gesundheits- und Behinderungsscore (höher = bessere Ergebnisse)', 'Living Environment Score': 'Qualität der Innen- und Außenumgebung (höher = besser)', 'Indoors Sub-domain Score': 'Wohnqualität und -zustand (höher = besser)', 'Outdoors Sub-domain Score': 'Luftqualität und Verkehrssicherheit (höher = besser)', - 'Serious crime per 1k residents (avg/yr)': 'Rate schwerer Straftaten pro 1.000 Einwohner pro Jahr', - 'Minor crime per 1k residents (avg/yr)': 'Rate leichter Straftaten pro 1.000 Einwohner pro Jahr', + 'Serious crime per 1k residents (avg/yr)': + 'Rate schwerer Straftaten pro 1.000 Einwohner pro Jahr', + 'Minor crime per 1k residents (avg/yr)': + 'Rate leichter Straftaten pro 1.000 Einwohner pro Jahr', 'Serious crime (avg/yr)': 'Summe der schweren Straftaten-Kategorien pro Jahr', 'Minor crime (avg/yr)': 'Summe der leichten Straftaten-Kategorien pro Jahr', - 'Violence and sexual offences (avg/yr)': 'Jährlicher Durchschnitt der Gewalt- und Sexualdelikte in der Gegend', + 'Violence and sexual offences (avg/yr)': + 'Jährlicher Durchschnitt der Gewalt- und Sexualdelikte in der Gegend', 'Burglary (avg/yr)': 'Jährlicher Durchschnitt der Einbrüche in der Gegend', 'Robbery (avg/yr)': 'Jährlicher Durchschnitt der Raubüberfälle in der Gegend', 'Vehicle crime (avg/yr)': 'Jährlicher Durchschnitt der Fahrzeugkriminalität in der Gegend', - 'Anti-social behaviour (avg/yr)': 'Jährlicher Durchschnitt des antisozialen Verhaltens in der Gegend', - 'Criminal damage and arson (avg/yr)': 'Jährlicher Durchschnitt der Sachbeschädigungen und Brandstiftungen in der Gegend', + 'Anti-social behaviour (avg/yr)': + 'Jährlicher Durchschnitt des antisozialen Verhaltens in der Gegend', + 'Criminal damage and arson (avg/yr)': + 'Jährlicher Durchschnitt der Sachbeschädigungen und Brandstiftungen in der Gegend', 'Other theft (avg/yr)': 'Jährlicher Durchschnitt des sonstigen Diebstahls in der Gegend', 'Theft from the person (avg/yr)': 'Jährlicher Durchschnitt des Taschendiebstahls in der Gegend', 'Shoplifting (avg/yr)': 'Jährlicher Durchschnitt des Ladendiebstahls in der Gegend', 'Bicycle theft (avg/yr)': 'Jährlicher Durchschnitt des Fahrraddiebstahls in der Gegend', 'Drugs (avg/yr)': 'Jährlicher Durchschnitt der Drogendelikte in der Gegend', - 'Possession of weapons (avg/yr)': 'Jährlicher Durchschnitt der Waffenbesitzdelikte in der Gegend', - 'Public order (avg/yr)': 'Jährlicher Durchschnitt der Störungen der öffentlichen Ordnung in der Gegend', + 'Possession of weapons (avg/yr)': + 'Jährlicher Durchschnitt der Waffenbesitzdelikte in der Gegend', + 'Public order (avg/yr)': + 'Jährlicher Durchschnitt der Störungen der öffentlichen Ordnung in der Gegend', 'Other crime (avg/yr)': 'Jährlicher Durchschnitt sonstiger Straftaten in der Gegend', 'Median age': 'Medianalter der lokalen Bevölkerung', '% White': 'Anteil der Bevölkerung, der sich als Weiß identifiziert', '% South Asian': 'Anteil der Bevölkerung, der sich als Südasiatisch identifiziert', '% Black': 'Anteil der Bevölkerung, der sich als Schwarz identifiziert', '% East Asian': 'Anteil der Bevölkerung, der sich als Ostasiatisch identifiziert', - '% Mixed': 'Anteil der Bevölkerung, der sich als gemischt oder mehreren ethnischen Gruppen zugehörig identifiziert', + '% Mixed': + 'Anteil der Bevölkerung, der sich als gemischt oder mehreren ethnischen Gruppen zugehörig identifiziert', '% Other': 'Anteil der Bevölkerung, der sich einer anderen ethnischen Gruppe zuordnet', 'Distance to nearest park (km)': 'Entfernung zum nächsten Park oder Grünfläche', 'Number of parks within 2km': 'Anzahl Parks und Grünflächen im Umkreis von 2 km', 'Number of restaurants within 2km': 'Anzahl Restaurants und Cafés im Umkreis von 2 km', - 'Number of grocery shops and supermarkets within 2km': 'Anzahl Lebensmittelgeschäfte und Supermärkte im Umkreis von 2 km', + 'Number of grocery shops and supermarkets within 2km': + 'Anzahl Lebensmittelgeschäfte und Supermärkte im Umkreis von 2 km', 'Noise (dB)': 'Straßenlärmpegel an der Postleitzahl in Dezibel (Lden)', - 'Max available download speed (Mbps)': 'Maximal verfügbare Breitband-Downloadgeschwindigkeit an der Postleitzahl', + 'Max available download speed (Mbps)': + 'Maximal verfügbare Breitband-Downloadgeschwindigkeit an der Postleitzahl', }, zh: { 'Listing status': '该房产是历史销售、当前在售还是出租', @@ -159,8 +195,8 @@ const descriptions: Record> = { 'Asking rent (monthly)': '当前出租房产的挂牌月租', 'Total floor area (sqm)': 'EPC评估的室内建筑面积', 'Number of bedrooms & living rooms': 'EPC评估的宜居房间数', - 'Bedrooms': '在线房源中的卧室数量', - 'Bathrooms': '在线房源中的浴室数量', + Bedrooms: '在线房源中的卧室数量', + Bathrooms: '在线房源中的浴室数量', 'Construction year': 'EPC评估的建造年份', 'Date of last transaction': 'Land Registry记录的最近一次销售日期', 'Listing date': '房产首次在线上市的日期', @@ -226,24 +262,33 @@ const descriptions: Record> = { 'Asking rent (monthly)': 'A kiadó ingatlanok hirdetett havi bérleti díja', 'Total floor area (sqm)': 'Az EPC felmérésből származó belső alapterület', 'Number of bedrooms & living rooms': 'Lakószobák száma az EPC felmérés alapján', - 'Bedrooms': 'Hálószobák száma az online hirdetés szerint', - 'Bathrooms': 'Fürdőszobák száma az online hirdetés szerint', + Bedrooms: 'Hálószobák száma az online hirdetés szerint', + Bathrooms: 'Fürdőszobák száma az online hirdetés szerint', 'Construction year': 'Becsült építési év az EPC alapján', 'Date of last transaction': 'Az utolsó eladás dátuma a Land Registry szerint', 'Listing date': 'Az ingatlan első online megjelenésének dátuma', 'Former council house': 'Az ingatlan szerepelt-e valaha önkormányzati lakásként', 'Current energy rating': 'Jelenlegi EPC energiabesorolás (A = legjobb, G = legrosszabb)', - 'Potential energy rating': 'Potenciális EPC besorolás az összes javasolt fejlesztés elvégzése után', + 'Potential energy rating': + 'Potenciális EPC besorolás az összes javasolt fejlesztés elvégzése után', 'Interior height (m)': 'Átlagos belmagasság az EPC felmérés alapján', - 'Distance to nearest train or tube station (km)': 'Távolság a legközelebbi vasút- vagy metróállomásig', - 'Good+ primary schools within 2km': 'Ofsted által Jó vagy Kiváló minősítésű általános iskolák 2 km-en belül', - 'Good+ secondary schools within 2km': 'Ofsted által Jó vagy Kiváló minősítésű középiskolák 2 km-en belül', - 'Good+ primary schools within 5km': 'Ofsted által Jó vagy Kiváló minősítésű általános iskolák 5 km-en belül', - 'Good+ secondary schools within 5km': 'Ofsted által Jó vagy Kiváló minősítésű középiskolák 5 km-en belül', - 'Education, Skills and Training Score': 'A környék oktatási minőségi pontszáma (magasabb = jobb)', + 'Distance to nearest train or tube station (km)': + 'Távolság a legközelebbi vasút- vagy metróállomásig', + 'Good+ primary schools within 2km': + 'Ofsted által Jó vagy Kiváló minősítésű általános iskolák 2 km-en belül', + 'Good+ secondary schools within 2km': + 'Ofsted által Jó vagy Kiváló minősítésű középiskolák 2 km-en belül', + 'Good+ primary schools within 5km': + 'Ofsted által Jó vagy Kiváló minősítésű általános iskolák 5 km-en belül', + 'Good+ secondary schools within 5km': + 'Ofsted által Jó vagy Kiváló minősítésű középiskolák 5 km-en belül', + 'Education, Skills and Training Score': + 'A környék oktatási minőségi pontszáma (magasabb = jobb)', 'Income Score (rate)': 'Jövedelmi deprivációs ráta, invertálva (magasabb = kevésbé hátrányos)', - 'Employment Score (rate)': 'Foglalkoztatási deprivációs ráta, invertálva (magasabb = kevésbé hátrányos)', - 'Health Deprivation and Disability Score': 'Egészségügyi és fogyatékossági pontszám (magasabb = jobb eredmények)', + 'Employment Score (rate)': + 'Foglalkoztatási deprivációs ráta, invertálva (magasabb = kevésbé hátrányos)', + 'Health Deprivation and Disability Score': + 'Egészségügyi és fogyatékossági pontszám (magasabb = jobb eredmények)', 'Living Environment Score': 'Belső és külső környezet minősége (magasabb = jobb)', 'Indoors Sub-domain Score': 'Lakásminőség és állapot (magasabb = jobb)', 'Outdoors Sub-domain Score': 'Levegőminőség és közlekedésbiztonság (magasabb = jobb)', @@ -251,7 +296,8 @@ const descriptions: Record> = { 'Minor crime per 1k residents (avg/yr)': 'Kisebb bűncselekmények aránya 1000 lakosra évente', 'Serious crime (avg/yr)': 'Súlyos bűncselekményi kategóriák éves összesítése', 'Minor crime (avg/yr)': 'Kisebb bűncselekményi kategóriák éves összesítése', - 'Violence and sexual offences (avg/yr)': 'Erőszakos és szexuális bűncselekmények éves átlaga a környéken', + 'Violence and sexual offences (avg/yr)': + 'Erőszakos és szexuális bűncselekmények éves átlaga a környéken', 'Burglary (avg/yr)': 'Betörések éves átlaga a környéken', 'Robbery (avg/yr)': 'Rablások éves átlaga a környéken', 'Vehicle crime (avg/yr)': 'Gépjárművel kapcsolatos bűncselekmények éves átlaga a környéken', @@ -262,7 +308,8 @@ const descriptions: Record> = { 'Shoplifting (avg/yr)': 'Bolti lopások éves átlaga a környéken', 'Bicycle theft (avg/yr)': 'Kerékpárlopások éves átlaga a környéken', 'Drugs (avg/yr)': 'Kábítószerrel kapcsolatos bűncselekmények éves átlaga a környéken', - 'Possession of weapons (avg/yr)': 'Fegyvertartással kapcsolatos bűncselekmények éves átlaga a környéken', + 'Possession of weapons (avg/yr)': + 'Fegyvertartással kapcsolatos bűncselekmények éves átlaga a környéken', 'Public order (avg/yr)': 'Közrend elleni bűncselekmények éves átlaga a környéken', 'Other crime (avg/yr)': 'Egyéb bűncselekmények éves átlaga a környéken', 'Median age': 'A helyi lakosság medián életkora', @@ -275,9 +322,11 @@ const descriptions: Record> = { 'Distance to nearest park (km)': 'Távolság a legközelebbi parkig vagy zöldterületig', 'Number of parks within 2km': 'Parkok és zöldterületek száma 2 km-en belül', 'Number of restaurants within 2km': 'Éttermek és kávézók száma 2 km-en belül', - 'Number of grocery shops and supermarkets within 2km': 'Élelmiszerboltok és szupermarketek száma 2 km-en belül', + 'Number of grocery shops and supermarkets within 2km': + 'Élelmiszerboltok és szupermarketek száma 2 km-en belül', 'Noise (dB)': 'Közúti zajszint az irányítószámnál decibelben (Lden)', - 'Max available download speed (Mbps)': 'Az irányítószámnál elérhető maximális szélessávú letöltési sebesség', + 'Max available download speed (Mbps)': + 'Az irányítószámnál elérhető maximális szélessávú letöltési sebesség', }, }; @@ -299,5 +348,5 @@ export function tsDesc(featureName: string, englishFromServer: string): string { export function tsDetail(featureName: string, englishFromServer: string): string { const lang = i18n.language; if (lang === 'en') return englishFromServer; - return details[lang]?.[featureName] ?? englishFromServer; + return descriptions[lang]?.[featureName] ?? englishFromServer; } diff --git a/frontend/src/i18n/details.ts b/frontend/src/i18n/details.ts new file mode 100644 index 0000000..8fa56e0 --- /dev/null +++ b/frontend/src/i18n/details.ts @@ -0,0 +1,530 @@ +/** + * Feature detail translations (the longer explanatory paragraph in the info card). + * Same structure as descriptions: keyed by language, then by feature name. + * English details come from the server — NOT duplicated here. + */ +export const details: Record> = { + fr: { + 'Listing status': + "Indique la source de l'enregistrement de la propriété : « Vente historique » provenant des données HM Land Registry Price Paid, « À vendre » provenant des annonces d'achat en ligne actuelles, ou « À louer » provenant des annonces de location en ligne actuelles.", + 'Property type': + 'Provient des données HM Land Registry Price Paid et des certificats EPC. Individuelle, Semi-individuelle, Mitoyenne (inclut tous les sous-types de maisons en rangée), Appartements/Maisons duplex, ou Autre (bungalows, mobil-homes, etc.).', + 'Leasehold/Freehold': + "Provient des données HM Land Registry Price Paid. Freehold signifie que vous êtes propriétaire du bâtiment et du terrain sur lequel il se trouve. Leasehold signifie que vous êtes propriétaire du bâtiment mais pas du terrain : vous disposez d'un bail accordé par le propriétaire du terrain pour un nombre d'années déterminé.", + 'Last known price': + "Le dernier prix de vente enregistré pour ce bien provenant des données HM Land Registry Price Paid. Couvre les ventes résidentielles en Angleterre. Peut dater de plusieurs années si le bien n'a pas été vendu récemment.", + 'Estimated current price': + "Basé sur le dernier prix de vente, ajusté en fonction des évolutions locales des prix au fil du temps à l'aide d'un indice de ventes répétées (suivi par secteur de code postal et type de bien). Si des améliorations postérieures à la vente sont détectées d'après les relevés EPC, une prime de rénovation est ajoutée. Les ventes récentes seront proches du prix d'origine ; les ventes plus anciennes font l'objet d'un ajustement plus important.", + 'Asking price': + "Le prix demandé tel qu'annoncé sur les portails immobiliers en ligne. Disponible uniquement pour les annonces « À vendre ».", + 'Price per sqm': + 'Calculé en divisant le dernier prix de vente connu par la surface habitable totale indiquée dans le certificat EPC. Utile pour comparer la valeur entre des biens de tailles différentes. Disponible uniquement lorsque les données de prix et de surface existent toutes les deux.', + 'Est. price per sqm': + "Calculé en divisant le prix actuel estimé et ajusté à l'inflation (y compris toute prime de rénovation) par la surface habitable totale indiquée dans le certificat EPC. Fournit une comparaison prix/superficie plus actualisée que le prix au sqm basé sur le prix de vente historique.", + 'Asking price per sqm': + 'Calculé en divisant le prix demandé affiché par la surface habitable totale. Disponible uniquement pour les biens actuellement mis en vente pour lesquels les données de surface existent.', + 'Estimated monthly rent': + "Prix médian mensuel de location provenant des statistiques sommaires du marché locatif privé de l'ONS (octobre 2022 - septembre 2023), correspondant à l'autorité locale et au nombre de chambres. Basé sur les données de locations de l'Agence d'évaluation (Valuation Office Agency).", + 'Asking rent (monthly)': + 'Le prix de location annoncé sur les portails immobiliers en ligne, converti en montant mensuel si nécessaire (par exemple, annonces hebdomadaires ou annuelles). Disponible uniquement pour les annonces « À louer ».', + 'Total floor area (sqm)': + "Surface habitable totale en mètres carrés telle que mesurée lors de l'évaluation du certificat de performance énergétique (EPC). Inclut toutes les pièces habitables mais exclut les garages, dépendances et espaces extérieurs.", + 'Number of bedrooms & living rooms': + "Nombre total de pièces habitables (chambres et salons) tel qu'enregistré dans le certificat de performance énergétique (EPC). Les cuisines et salles de bain sont généralement exclues, sauf si elles sont suffisamment grandes pour être comptées comme pièces habitables.", + Bedrooms: + "Nombre de chambres tel qu'annoncé dans l'annonce immobilière en ligne. Renseigné uniquement pour les annonces en ligne (vente et location) ; nul pour les ventes historiques.", + Bathrooms: + "Nombre de salles de bain tel qu'annoncé dans l'annonce immobilière en ligne. Renseigné uniquement pour les annonces en ligne (vente et location) ; nul pour les ventes historiques.", + 'Construction year': + "Dérivé de la tranche d'âge de construction indiquée dans l'EPC (par exemple « 1930-1949 ») en prenant le point médian. Moins précis pour les bâtiments anciens où la tranche d'âge s'étend sur plusieurs décennies.", + 'Date of last transaction': + 'La date de la vente enregistrée la plus récente pour ce bien, provenant des données HM Land Registry Price Paid. Stockée sous forme de date/heure dans les données ; convertie en année fractionnaire pour le filtrage et les graphiques.', + 'Listing date': + "La date à laquelle l'annonce immobilière est apparue pour la première fois sur le portail immobilier en ligne. Stockée sous forme de date/heure ; convertie en année fractionnaire pour le filtrage. Renseignée uniquement pour les annonces en ligne.", + 'Former council house': + "Dérivé du champ TENURE dans les données du certificat de performance énergétique (EPC). Si l'un des certificats EPC pour ce bien a enregistré le régime d'occupation comme location sociale, cela indique que le bien faisait partie du parc de logements du conseil municipal ou d'une association de logement au moment de cette inspection. Les biens qui ont été vendus ultérieurement (par exemple via le Right to Buy) conservent cet indicateur.", + 'Current energy rating': + "La note d'efficacité énergétique actuelle issue du certificat de performance énergétique (EPC). Va de A (plus efficace) à G (moins efficace). Basée sur la consommation d'énergie du bien par mètre carré de surface habitable.", + 'Potential energy rating': + "La note d'efficacité énergétique potentielle issue du certificat de performance énergétique (EPC), si toutes les améliorations rentables recommandées dans le rapport EPC étaient réalisées. Va de A (plus efficace) à G (moins efficace).", + 'Interior height (m)': + "Hauteur intérieure moyenne (sol au plafond) en mètres telle qu'enregistrée lors de l'évaluation du certificat de performance énergétique (EPC). Calculée en divisant le volume intérieur total par la surface habitable totale.", + 'Distance to nearest train or tube station (km)': + "Distance à vol d'oiseau en kilomètres depuis le code postal jusqu'à la gare ferroviaire ou la station de métro/tram la plus proche.", + 'Good+ primary schools within 2km': + "Écoles primaires financées par l'État dans un rayon de 2km ayant une note Ofsted actuelle de Bon ou Exceptionnel. Les écoles n'ayant pas encore été inspectées sont exclues.", + 'Good+ secondary schools within 2km': + "Lycées et collèges financés par l'État dans un rayon de 2km ayant une note Ofsted actuelle de Bon ou Exceptionnel. Les établissements n'ayant pas encore été inspectés sont exclus.", + 'Good+ primary schools within 5km': + "Écoles primaires financées par l'État dans un rayon de 5km ayant une note Ofsted actuelle de Bon ou Exceptionnel. Les écoles n'ayant pas encore été inspectées sont exclues.", + 'Good+ secondary schools within 5km': + "Lycées et collèges financés par l'État dans un rayon de 5km ayant une note Ofsted actuelle de Bon ou Exceptionnel. Les établissements n'ayant pas encore été inspectés sont exclus.", + 'Education, Skills and Training Score': + "Provient des Indices de Déprivation anglais (inversé afin que plus le score est élevé, meilleur est le résultat). Couvre les résultats scolaires, l'accès à l'enseignement supérieur, les qualifications des adultes et la maîtrise de la langue anglaise. Des scores plus élevés indiquent moins de déprivation.", + 'Income Score (rate)': + "Provient des Indices de Déprivation anglais (inversé afin que plus le score est élevé, meilleur est le résultat). Des valeurs plus élevées indiquent moins de déprivation de revenus. Basé sur les allocations de soutien au revenu, l'allocation de demandeur d'emploi sous condition de ressources, l'allocation d'emploi et de soutien sous condition de ressources, le crédit de retraite, le crédit d'impôt pour le travail et les enfants, l'Universal Credit et les demandeurs d'asile.", + 'Employment Score (rate)': + "Provient des Indices de Déprivation anglais (inversé afin que plus le score est élevé, meilleur est le résultat). Des valeurs plus élevées indiquent moins de déprivation d'emploi. Basé sur les allocataires de l'allocation de demandeur d'emploi, de l'allocation d'emploi et de soutien, de l'allocation d'incapacité, de l'allocation de handicap sévère, de l'allocation d'aidant et les bénéficiaires pertinents de l'Universal Credit.", + 'Health Deprivation and Disability Score': + "Provient des Indices de Déprivation anglais (inversé afin que plus le score est élevé, meilleur est le résultat). Des scores plus élevés indiquent un risque de décès prématuré plus faible et une meilleure qualité de vie. Dérivé des années de vie potentielle perdues, du ratio comparatif de maladie et d'invalidité, de la morbidité aiguë et des troubles de l'humeur et d'anxiété.", + 'Living Environment Score': + "Provient des Indices de Déprivation anglais (inversé afin que plus le score est élevé, meilleur est le résultat). Combine la qualité du logement (état, chauffage central) et l'environnement extérieur (qualité de l'air, sécurité routière). Des scores plus élevés indiquent de meilleurs environnements de vie.", + 'Indoors Sub-domain Score': + 'Provient des Indices de Déprivation anglais, domaine Environnement de Vie (inversé afin que plus le score est élevé, meilleur est le résultat). Mesure la qualité du parc immobilier : disponibilité du chauffage central, état des logements et normes Decent Homes. Des scores plus élevés indiquent de meilleures conditions de logement.', + 'Outdoors Sub-domain Score': + "Provient des Indices de Déprivation anglais, domaine Environnement de Vie (inversé afin que plus le score est élevé, meilleur est le résultat). Mesure la qualité de l'environnement de vie extérieur à travers des indicateurs de qualité de l'air et les victimes d'accidents de la route impliquant des piétons et des cyclistes. Des scores plus élevés indiquent de meilleurs environnements extérieurs.", + 'Serious crime per 1k residents (avg/yr)': + "Violences, braquages, cambriolages et possession d'armes pour 1 000 résidents habituels par an dans le LSOA. Utilise les données de criminalité au niveau de la rue de police.uk (2023-2025) et les décomptes de population du Census 2021. Normalise en fonction de la densité de population afin que les zones soient comparables quelle que soit leur taille.", + 'Minor crime per 1k residents (avg/yr)': + "Comportements antisociaux, vols à l'étalage, vols de vélos et autres crimes de moindre gravité pour 1 000 résidents habituels par an dans le LSOA. Utilise les données de criminalité au niveau de la rue de police.uk (2023-2025) et les décomptes de population du Census 2021. Normalise en fonction de la densité de population afin que les zones soient comparables quelle que soit leur taille.", + 'Serious crime (avg/yr)': + "Somme des violences, braquages, cambriolages et possessions d'armes par an dans le LSOA, provenant des données de criminalité au niveau de la rue de police.uk (2023-2025). Fournit un indicateur unique de criminalité grave.", + 'Minor crime (avg/yr)': + "Somme des comportements antisociaux, vols à l'étalage, vols de vélos et autres crimes de moindre gravité par an dans le LSOA, provenant des données de criminalité au niveau de la rue de police.uk (2023-2025). Fournit un indicateur unique de criminalité mineure.", + 'Violence and sexual offences (avg/yr)': + 'Nombre moyen de violences et infractions sexuelles par an dans le LSOA, provenant des données de criminalité au niveau de la rue de police.uk (2023-2025). Inclut les agressions, le harcèlement et les infractions sexuelles.', + 'Burglary (avg/yr)': + 'Nombre moyen de cambriolages par an dans le LSOA, provenant des données de criminalité au niveau de la rue de police.uk (2023-2025). Inclut les cambriolages résidentiels et commerciaux.', + 'Robbery (avg/yr)': + 'Nombre moyen de braquages par an dans le LSOA, provenant des données de criminalité au niveau de la rue de police.uk (2023-2025). Le braquage implique un vol avec usage ou menace de la force.', + 'Vehicle crime (avg/yr)': + "Nombre moyen d'incidents de criminalité liés aux véhicules par an dans le LSOA, provenant des données de criminalité au niveau de la rue de police.uk (2023-2025). Inclut le vol de véhicules et les vols à l'intérieur des véhicules.", + 'Anti-social behaviour (avg/yr)': + "Nombre moyen d'incidents de comportement antisocial par an dans le LSOA, provenant des données de criminalité au niveau de la rue de police.uk (2023-2025). Inclut les nuisances, les comportements antisociaux environnementaux et personnels.", + 'Criminal damage and arson (avg/yr)': + "Nombre moyen d'incidents de dommages criminels et d'incendie volontaire par an dans le LSOA, provenant des données de criminalité au niveau de la rue de police.uk (2023-2025).", + 'Other theft (avg/yr)': + "Nombre moyen d'infractions de « vol divers » par an dans le LSOA, provenant des données de criminalité au niveau de la rue de police.uk (2023-2025). Inclut les vols ne relevant pas des catégories cambriolage, criminalité liée aux véhicules, vol à l'étalage ou vol de vélos.", + 'Theft from the person (avg/yr)': + "Nombre moyen d'infractions de vol à la tire par an dans le LSOA, provenant des données de criminalité au niveau de la rue de police.uk (2023-2025). Inclut le pickpocket et l'arrachage de sac sans violence.", + 'Shoplifting (avg/yr)': + "Nombre moyen d'infractions de vol à l'étalage par an dans le LSOA, provenant des données de criminalité au niveau de la rue de police.uk (2023-2025).", + 'Bicycle theft (avg/yr)': + "Nombre moyen d'infractions de vol de vélos par an dans le LSOA, provenant des données de criminalité au niveau de la rue de police.uk (2023-2025).", + 'Drugs (avg/yr)': + "Nombre moyen d'infractions liées aux drogues par an dans le LSOA, provenant des données de criminalité au niveau de la rue de police.uk (2023-2025). Inclut les infractions de possession et de trafic.", + 'Possession of weapons (avg/yr)': + "Nombre moyen d'infractions de possession d'armes par an dans le LSOA, provenant des données de criminalité au niveau de la rue de police.uk (2023-2025).", + 'Public order (avg/yr)': + "Nombre moyen d'infractions à l'ordre public par an dans le LSOA, provenant des données de criminalité au niveau de la rue de police.uk (2023-2025). Inclut les actes causant de la peur, de l'alarme ou de la détresse.", + 'Other crime (avg/yr)': + "Nombre moyen d'autres infractions criminelles par an dans le LSOA, provenant des données de criminalité au niveau de la rue de police.uk (2023-2025). Catégorie fourre-tout pour les infractions non classées ailleurs.", + 'Median age': + "Provient du Census 2021 (TS007A). Âge médian des résidents habituels dans le LSOA, calculé par interpolation linéaire à partir des effectifs par tranche d'âge de cinq ans. Les zones à population plus jeune ont tendance à être urbaines, universitaires ou à accueillir davantage de familles ; les médianes plus élevées sont typiques des zones rurales et côtières.", + '% White': + "Provient du Census 2021. Pourcentage de la population de l'autorité locale s'identifiant comme Blanc (anglais, gallois, écossais, nord-irlandais, britannique, irlandais, Gitan ou Voyageur irlandais, Rom, ou tout autre origine blanche).", + '% South Asian': + "Provient du Census 2021. Pourcentage de la population de l'autorité locale s'identifiant comme Indien, Pakistanais, Bangladais ou toute autre origine asiatique.", + '% Black': + "Provient du Census 2021. Pourcentage de la population de l'autorité locale s'identifiant comme Noir, Noir britannique, Caribéen ou Africain.", + '% East Asian': + "Provient du Census 2021. Pourcentage de la population de l'autorité locale s'identifiant comme Chinois.", + '% Mixed': + "Provient du Census 2021. Pourcentage de la population de l'autorité locale s'identifiant comme Mixte ou appartenant à plusieurs groupes ethniques (Blanc et Noir caribéen, Blanc et Noir africain, Blanc et Asiatique, ou tout autre fond mixte ou multiple).", + '% Other': + "Provient du Census 2021. Pourcentage de la population de l'autorité locale s'identifiant comme appartenant à un autre groupe ethnique (Arabe ou tout autre groupe ethnique non couvert par les catégories principales).", + 'Distance to nearest park (km)': + "Distance à vol d'oiseau en kilomètres depuis le code postal jusqu'à l'entrée du parc la plus proche. Couvre les parcs publics, jardins, terrains de jeux et espaces de loisirs. Utilise les emplacements des points d'accès issus du jeu de données OS Open Greenspace, de sorte que les propriétés bordant un grand parc affichent correctement une courte distance.", + 'Number of parks within 2km': + 'Nombre de parcs publics, jardins, terrains de jeux et espaces de loisirs dont au moins une entrée se trouve dans un rayon de 2km du centroïde du code postal de la propri��té. Dérivé du jeu de données OS Open Greenspace (Ordnance Survey), utilisant les emplacements des entrées de parcs pour une correspondance de proximité précise.', + 'Number of restaurants within 2km': + 'Restaurants, cafés et établissements de restauration dans un rayon de 2km du code postal. Source : OpenStreetMap.', + 'Number of grocery shops and supermarkets within 2km': + "Nombre de supermarchés, épiceries et autres commerces alimentaires dans un rayon de 2km du centroïde du code postal de la propriété. Dérivé des données POI d'OpenStreetMap.", + 'Noise (dB)': + "Niveau de bruit routier en décibels (Lden, moyenne pondérée sur 24 heures) provenant de la cartographie stratégique du bruit de Defra, 4e cycle (2022). Modélisé à 4m au-dessus du sol sur une grille de 10m. Au-dessus d'environ 55 dB, le bruit est généralement perceptible ; au-dessus d'environ 70 dB, il est considéré comme nocif par l'OMS.", + 'Max available download speed (Mbps)': + "Vitesse de téléchargement fixe maximale disponible auprès de n'importe quel fournisseur, provenant d'Ofcom Connected Nations 2025. Représente le maximum théorique, et non les vitesses réellement atteintes. 10 Mbps = basique, 30 = superrapide, 100+ = ultra-rapide, 1000 = gigabit.", + }, + de: { + 'Listing status': + 'Gibt die Quelle des Immobilieneintrags an: „Historical sale" aus den HM Land Registry Price Paid-Daten, „For sale" aus aktuellen Online-Kaufangeboten oder „For rent" aus aktuellen Online-Mietangeboten.', + 'Property type': + 'Aus den HM Land Registry Price Paid-Daten und EPC-Zertifikaten. Freistehend, Doppelhaushälfte, Reihenhaus (umfasst alle Untertypen), Wohnungen/Maisonettes oder Sonstiges (Bungalows, Mobilheime usw.).', + 'Leasehold/Freehold': + 'Aus den HM Land Registry Price Paid-Daten. Freehold bedeutet, dass Sie das Gebäude und das Grundstück besitzen. Leasehold bedeutet, dass Sie das Gebäude, aber nicht das Grundstück besitzen: Sie haben einen Pachtvertrag vom Freeholder für eine festgelegte Anzahl von Jahren.', + 'Last known price': + 'Der zuletzt erfasste Verkaufspreis für diese Immobilie aus den HM Land Registry Price Paid-Daten. Umfasst Wohnimmobilienverkäufe in England. Kann Jahre alt sein, wenn die Immobilie nicht kürzlich verkauft wurde.', + 'Estimated current price': + 'Basiert auf dem letzten Verkaufspreis, angepasst an lokale Preisveränderungen im Laufe der Zeit mithilfe eines Repeat-Sales-Index (erfasst pro Postleitzahlensektor und Immobilientyp). Wenn nach dem Verkauf durchgeführte Renovierungen aus EPC-Aufzeichnungen erkennbar sind, wird ein Renovierungsaufschlag hinzugefügt. Kürzliche Verkäufe liegen nahe am ursprünglichen Preis; ältere Verkäufe werden stärker angepasst.', + 'Asking price': + 'Der beworbene Angebotspreis aus Online-Immobilienportalen. Nur für „For sale"-Angebote verfügbar.', + 'Price per sqm': + 'Berechnet durch Division des zuletzt bekannten Verkaufspreises durch die Gesamtnutzfläche aus dem EPC-Zertifikat. Nützlich zum Vergleich des Wertes verschiedener Immobiliengrößen. Nur verfügbar, wenn sowohl Preis- als auch Flächendaten vorhanden sind.', + 'Est. price per sqm': + 'Berechnet durch Division des inflationsbereinigten geschätzten aktuellen Preises (einschließlich etwaiger Renovierungsaufschläge) durch die Gesamtnutzfläche aus dem EPC-Zertifikat. Bietet einen aktuelleren Preis-pro-Fläche-Vergleich als der historische Verkaufspreis pro sqm.', + 'Asking price per sqm': + 'Berechnet durch Division des angebotenen Kaufpreises durch die Gesamtnutzfläche. Nur für zum Verkauf angebotene Immobilien verfügbar, für die Flächendaten vorhanden sind.', + 'Estimated monthly rent': + 'Monatlicher Median-Mietpreis aus den ONS Private Rental Market Summary Statistics (Okt. 2022 – Sep. 2023), abgeglichen nach Gemeinde und Zimmeranzahl. Basiert auf Vermietungsdaten der Valuation Office Agency.', + 'Asking rent (monthly)': + 'Der beworbene Mietpreis aus Online-Immobilienportalen, bei Bedarf in einen monatlichen Betrag umgerechnet (z. B. bei wöchentlichen oder jährlichen Angeboten). Nur für „For rent"-Angebote verfügbar.', + 'Total floor area (sqm)': + 'Gesamte nutzbare Wohnfläche in Quadratmetern, gemessen während der Bewertung für das Energieausweis-Zertifikat. Umfasst alle Wohnräume, schließt jedoch Garagen, Nebengebäude und Außenbereiche aus.', + 'Number of bedrooms & living rooms': + 'Gesamtanzahl der Wohnräume (Schlaf- und Wohnzimmer), wie im Energieausweis-Zertifikat erfasst. Küchen und Badezimmer sind in der Regel ausgeschlossen, sofern sie nicht groß genug sind, um als Wohnräume zu gelten.', + Bedrooms: + 'Anzahl der Schlafzimmer, wie im Online-Immobilienangebot angegeben. Nur für Online-Angebote (Kauf und Miete) verfügbar; bei historischen Verkäufen nicht angegeben.', + Bathrooms: + 'Anzahl der Badezimmer, wie im Online-Immobilienangebot angegeben. Nur für Online-Angebote (Kauf und Miete) verfügbar; bei historischen Verkäufen nicht angegeben.', + 'Construction year': + 'Abgeleitet aus dem Baualtersband im EPC (z. B. „1930–1949") durch Verwendung des Mittelpunkts. Bei älteren Gebäuden, bei denen das Altersband mehrere Jahrzehnte umfasst, weniger präzise.', + 'Date of last transaction': + 'Das Datum des zuletzt erfassten Verkaufs dieser Immobilie aus den HM Land Registry Price Paid-Daten. In den Daten als Datum-/Uhrzeitangabe gespeichert; für Filterung und Diagramme in ein Dezimaljahr umgerechnet.', + 'Listing date': + 'Das Datum, an dem das Immobilienangebot erstmals auf dem Online-Immobilienportal erschien. Als Datum-/Uhrzeitangabe gespeichert; für die Filterung in ein Dezimaljahr umgerechnet. Nur für Online-Angebote verfügbar.', + 'Former council house': + 'Abgeleitet aus dem TENURE-Feld in den Energieausweis-Daten. Wenn für diese Immobilie ein EPC-Zertifikat das Nutzungsverhältnis als Sozialmiete erfasste, deutet dies darauf hin, dass die Immobilie zum Zeitpunkt dieser Inspektion Gemeinde- oder Wohnungsbaugesellschaftsbestand war. Immobilien, die später verkauft wurden (z. B. über Right to Buy), behalten dieses Merkmal.', + 'Current energy rating': + 'Die aktuelle Energieeffizienzklasse aus dem Energieausweis-Zertifikat. Reicht von A (am effizientesten) bis G (am wenigsten effizient). Basiert auf dem Energieverbrauch der Immobilie pro Quadratmeter Wohnfläche.', + 'Potential energy rating': + 'Die potenzielle Energieeffizienzklasse aus dem Energieausweis-Zertifikat, wenn alle im EPC-Bericht empfohlenen kosteneffizienten Verbesserungen durchgeführt würden. Reicht von A (am effizientesten) bis G (am wenigsten effizient).', + 'Interior height (m)': + 'Durchschnittliche lichte Raumhöhe in Metern, wie während der Energieausweis-Begutachtung erfasst. Berechnet durch Division des gesamten Innenvolumens durch die Gesamtwohnfläche.', + 'Distance to nearest train or tube station (km)': + 'Luftlinienentfernung in Kilometern vom Postleitzahlenzentrum zur nächsten Bahnstation oder U-Bahn-/Metro-/Straßenbahnhaltestelle.', + 'Good+ primary schools within 2km': + 'Staatlich geförderte Grundschulen innerhalb von 2 km mit einer aktuellen Ofsted-Bewertung von „Good" oder „Outstanding". Noch nicht inspizierte Schulen sind ausgeschlossen.', + 'Good+ secondary schools within 2km': + 'Staatlich geförderte weiterführende Schulen innerhalb von 2 km mit einer aktuellen Ofsted-Bewertung von „Good" oder „Outstanding". Noch nicht inspizierte Schulen sind ausgeschlossen.', + 'Good+ primary schools within 5km': + 'Staatlich geförderte Grundschulen innerhalb von 5 km mit einer aktuellen Ofsted-Bewertung von „Good" oder „Outstanding". Noch nicht inspizierte Schulen sind ausgeschlossen.', + 'Good+ secondary schools within 5km': + 'Staatlich geförderte weiterführende Schulen innerhalb von 5 km mit einer aktuellen Ofsted-Bewertung von „Good" oder „Outstanding". Noch nicht inspizierte Schulen sind ausgeschlossen.', + 'Education, Skills and Training Score': + 'Aus den englischen Deprivationsindizes (invertiert, sodass höher = besser bedeutet). Umfasst Schulleistungen, Hochschulzugang, Qualifikationen Erwachsener und Englischsprachkenntnisse. Höhere Werte weisen auf geringere Benachteiligung hin.', + 'Income Score (rate)': + "Aus den englischen Deprivationsindizes (invertiert, sodass höher = besser bedeutet). Höhere Werte weisen auf geringere Einkommensbenachteiligung hin. Basiert auf Income Support, einkommensbasiertem Jobseeker's Allowance, einkommensbasiertem Employment and Support Allowance, Pension Credit, Working Tax Credit und Child Tax Credit, Universal Credit sowie Asylbewerbern.", + 'Employment Score (rate)': + "Aus den englischen Deprivationsindizes (invertiert, sodass höher = besser bedeutet). Höhere Werte weisen auf geringere Beschäftigungsbenachteiligung hin. Basiert auf Empfängern von Jobseeker's Allowance, Employment and Support Allowance, Incapacity Benefit, Severe Disablement Allowance, Carer's Allowance und relevanten Universal Credit-Empfängern.", + 'Health Deprivation and Disability Score': + 'Aus den englischen Deprivationsindizes (invertiert, sodass höher = besser bedeutet). Höhere Werte weisen auf ein geringeres Risiko eines vorzeitigen Todes und eine bessere Lebensqualität hin. Abgeleitet aus verlorenen Lebensjahren, vergleichender Krankheits- und Behinderungsquote, akuter Morbidität sowie Stimmungs- und Angststörungen.', + 'Living Environment Score': + 'Aus den englischen Deprivationsindizes (invertiert, sodass höher = besser bedeutet). Kombiniert Wohnqualität (Zustand, Zentralheizung) und Außenumgebung (Luftqualität, Verkehrssicherheit). Höhere Werte weisen auf bessere Wohnumgebungen hin.', + 'Indoors Sub-domain Score': + 'Aus den englischen Deprivationsindizes, Bereich Wohnumgebung (invertiert, sodass höher = besser bedeutet). Misst die Qualität des Wohnungsbestands: Verfügbarkeit von Zentralheizung, Wohnungszustand und Decent Homes-Standards. Höhere Werte weisen auf bessere Wohnbedingungen hin.', + 'Outdoors Sub-domain Score': + 'Aus den englischen Deprivationsindizes, Bereich Wohnumgebung (invertiert, sodass höher = besser bedeutet). Misst die Qualität der Außenwohnumgebung anhand von Luftqualitätsindikatoren und Straßenverkehrsunfällen mit Fußgängern und Radfahrern. Höhere Werte weisen auf bessere Außenumgebungen hin.', + 'Serious crime per 1k residents (avg/yr)': + 'Gewalt, Raub, Einbruch und Waffenbesitz pro 1.000 Einwohner pro Jahr im LSOA. Verwendet police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025) und Census 2021-Bevölkerungszahlen. Normalisiert nach Bevölkerungsdichte, sodass Gebiete unabhängig von ihrer Größe vergleichbar sind.', + 'Minor crime per 1k residents (avg/yr)': + 'Asoziales Verhalten, Ladendiebstahl, Fahrraddiebstahl und andere weniger schwere Straftaten pro 1.000 Einwohner pro Jahr im LSOA. Verwendet police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025) und Census 2021-Bevölkerungszahlen. Normalisiert nach Bevölkerungsdichte, sodass Gebiete unabhängig von ihrer Größe vergleichbar sind.', + 'Serious crime (avg/yr)': + 'Summe aus Gewalt, Raub, Einbruch und Waffenbesitz pro Jahr im LSOA, aus police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025). Bietet einen einzelnen Indikator für schwere Kriminalität.', + 'Minor crime (avg/yr)': + 'Summe aus asozialem Verhalten, Ladendiebstahl, Fahrraddiebstahl und anderen weniger schweren Straftaten pro Jahr im LSOA, aus police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025). Bietet einen einzelnen Indikator für leichte Kriminalität.', + 'Violence and sexual offences (avg/yr)': + 'Durchschnittliche Anzahl von Gewalt- und Sexualdelikten pro Jahr im LSOA, aus police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025). Umfasst Körperverletzung, Belästigung und Sexualdelikte.', + 'Burglary (avg/yr)': + 'Durchschnittliche Anzahl von Einbruchsdelikten pro Jahr im LSOA, aus police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025). Umfasst Wohnungs- und Gewerbeeinbrüche.', + 'Robbery (avg/yr)': + 'Durchschnittliche Anzahl von Raubdelikten pro Jahr im LSOA, aus police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025). Raub umfasst Diebstahl unter Anwendung von Gewalt oder Gewaltandrohung.', + 'Vehicle crime (avg/yr)': + 'Durchschnittliche Anzahl von Fahrzeugkriminalität pro Jahr im LSOA, aus police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025). Umfasst Diebstahl von und aus Fahrzeugen.', + 'Anti-social behaviour (avg/yr)': + 'Durchschnittliche Anzahl von Vorfällen asozialen Verhaltens pro Jahr im LSOA, aus police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025). Umfasst störendes, umweltbezogenes und persönlich asoziales Verhalten.', + 'Criminal damage and arson (avg/yr)': + 'Durchschnittliche Anzahl von Sachbeschädigungs- und Brandstiftungsvorfällen pro Jahr im LSOA, aus police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025).', + 'Other theft (avg/yr)': + 'Durchschnittliche Anzahl von „sonstigen Diebstählen" pro Jahr im LSOA, aus police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025). Umfasst Diebstähle, die nicht unter Einbruch, Fahrzeugkriminalität, Ladendiebstahl oder Fahrraddiebstahl eingestuft sind.', + 'Theft from the person (avg/yr)': + 'Durchschnittliche Anzahl von Taschendiebstählen und ähnlichen Delikten pro Jahr im LSOA, aus police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025). Umfasst Taschendiebstahl und Handtaschenraub ohne Gewaltanwendung.', + 'Shoplifting (avg/yr)': + 'Durchschnittliche Anzahl von Ladendiebstahlsdelikten pro Jahr im LSOA, aus police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025).', + 'Bicycle theft (avg/yr)': + 'Durchschnittliche Anzahl von Fahrraddiebstahlsdelikten pro Jahr im LSOA, aus police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025).', + 'Drugs (avg/yr)': + 'Durchschnittliche Anzahl von Drogendelikten pro Jahr im LSOA, aus police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025). Umfasst Besitz- und Handelsdelikte.', + 'Possession of weapons (avg/yr)': + 'Durchschnittliche Anzahl von Waffenbesitzdelikten pro Jahr im LSOA, aus police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025).', + 'Public order (avg/yr)': + 'Durchschnittliche Anzahl von Delikten gegen die öffentliche Ordnung pro Jahr im LSOA, aus police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025). Umfasst das Verursachen von Furcht, Alarm oder Bedrängnis.', + 'Other crime (avg/yr)': + 'Durchschnittliche Anzahl sonstiger Straftaten pro Jahr im LSOA, aus police.uk-Kriminalitätsdaten auf Straßenebene (2023–2025). Eine Sammelkategorie für Straftaten, die nicht anderweitig eingestuft sind.', + 'Median age': + 'Aus dem Census 2021 (TS007A). Medianalter der ortsansässigen Bevölkerung im LSOA, berechnet durch lineare Interpolation aus Fünfjahres-Altersband-Zählungen. Gebiete mit jüngerer Bevölkerung sind tendenziell städtisch, Universitätsstädte oder haben mehr Familien; höhere Mediane sind typisch für ländliche und Küstengebiete.', + '% White': + 'Aus dem Census 2021. Prozentsatz der Bevölkerung der Gemeinde, die sich als Weiß identifiziert (Englisch, Walisisch, Schottisch, Nordirisch, Britisch, Irisch, Sinti und Roma, Roma oder sonstiger weißer Hintergrund).', + '% South Asian': + 'Aus dem Census 2021. Prozentsatz der Bevölkerung der Gemeinde, die sich als Indisch, Pakistanisch, Bangladeschisch oder mit sonstigem asiatischen Hintergrund identifiziert.', + '% Black': + 'Aus dem Census 2021. Prozentsatz der Bevölkerung der Gemeinde, die sich als Schwarz, Schwarz-Britisch, Karibisch oder Afrikanisch identifiziert.', + '% East Asian': + 'Aus dem Census 2021. Prozentsatz der Bevölkerung der Gemeinde, die sich als Chinesisch identifiziert.', + '% Mixed': + 'Aus dem Census 2021. Prozentsatz der Bevölkerung der Gemeinde, die sich als gemischt oder mit mehreren ethnischen Zugehörigkeiten identifiziert (Weiß und Schwarzkaribisch, Weiß und Schwarzafrikanisch, Weiß und Asiatisch oder sonstiger gemischter Hintergrund).', + '% Other': + 'Aus dem Census 2021. Prozentsatz der Bevölkerung der Gemeinde, die sich als einer anderen ethnischen Gruppe zugehörig identifiziert (Arabisch oder eine andere ethnische Gruppe, die nicht von den Hauptkategorien abgedeckt wird).', + 'Distance to nearest park (km)': + 'Luftlinienentfernung in Kilometern vom Postleitzahlenzentrum zum nächsten Parkeingang. Umfasst öffentliche Parks, Gärten, Sportplätze und Spielbereiche. Verwendet Zugangspunktstandorte aus dem OS Open Greenspace-Datensatz, sodass Immobilien an der Grenze eines großen Parks korrekt eine kurze Entfernung anzeigen.', + 'Number of parks within 2km': + 'Anzahl öffentlicher Parks, Gärten, Sportplätze und Spielbereiche mit mindestens einem Eingang innerhalb eines 2-km-Radius um den Postleitzahlenmittelpunkt der Immobilie. Abgeleitet aus dem OS Open Greenspace-Datensatz (Ordnance Survey) unter Verwendung von Parkeingangsstandorten für genaues Abstandsmatching.', + 'Number of restaurants within 2km': + 'Restaurants, Cafés und Gastronomiebetriebe innerhalb von 2 km vom Postleitzahlenzentrum. Bezogen aus OpenStreetMap.', + 'Number of grocery shops and supermarkets within 2km': + 'Anzahl von Supermärkten, Lebensmittelläden und anderen Lebensmittelgeschäften innerhalb eines 2-km-Radius um den Postleitzahlenmittelpunkt der Immobilie. Abgeleitet aus OpenStreetMap-POI-Daten.', + 'Noise (dB)': + 'Straßenlärmpegel in Dezibel (Lden, ein 24-Stunden-gewichteter Durchschnitt) aus Defras Strategic Noise Mapping Round 4 (2022). Modelliert in 4 m Höhe über dem Boden auf einem 10-m-Raster. Über ~55 dB ist in der Regel wahrnehmbar; über ~70 dB gilt laut WHO als gesundheitsschädlich.', + 'Max available download speed (Mbps)': + 'Maximale verfügbare Festnetz-Download-Geschwindigkeit von einem beliebigen Anbieter, aus Ofcom Connected Nations 2025. Gibt die theoretische Höchstgeschwindigkeit an, keine tatsächlich erreichten Geschwindigkeiten. 10 Mbps = Basis, 30 = Superfast, 100+ = Ultrafast, 1000 = Gigabit.', + }, + zh: { + 'Listing status': + '表示房产记录的来源:"Historical sale"来自英国土地注册局价格数据,"For sale"来自当前在线买卖房源,"For rent"来自当前在线租赁房源。', + 'Property type': + '来自英国土地注册局价格数据和EPC证书。包括独立式、半独立式、联排式(含所有联排子类型)、公寓/复式公寓,或其他类型(平房、移动式住宅等)。', + 'Leasehold/Freehold': + '来自英国土地注册局价格数据。Freehold(永久产权)意味着您拥有建筑物及其所在土地。Leasehold(租赁产权)意味着您拥有建筑物但不拥有土地:您从永久产权人处获得一定年限的租约。', + 'Last known price': + '来自英国土地注册局价格数据中该房产最近一次记录的成交价格。涵盖英格兰地区的住宅销售。若该房产近期未出售,数据可能已有数年之久。', + 'Estimated current price': + '基于最后一次成交价格,使用重复销售指数(按邮政编码区段和房产类型追踪)调整当地房价随时间的变化。若EPC记录显示售后有改造记录,则会增加装修溢价。近期销售与原价接近;较早的销售调整幅度更大。', + 'Asking price': '来自在线房产平台的挂牌要价。仅适用于"For sale"房源。', + 'Price per sqm': + '用最后已知成交价除以EPC证书中的总建筑面积计算得出。便于比较不同面积房产的价值。仅在价格和面积数据均存在时才可用。', + 'Est. price per sqm': + '用经通胀调整的估算当前价格(含装修溢价)除以EPC证书中的总建筑面积计算得出。与历史成交价格每平方米相比,提供更为最新的单位面积价格对比。', + 'Asking price per sqm': + '用挂牌要价除以总建筑面积计算得出。仅适用于当前在售且存在面积数据的房产。', + 'Estimated monthly rent': + '来自ONS私人租赁市场摘要统计(2022年10月至2023年9月)的月租金中位数,按地方政府和卧室数量匹配。基于估价署租赁数据。', + 'Asking rent (monthly)': + '来自在线房产平台的挂牌租金,如有需要会换算为月租金(例如按周或按年计价的房源)。仅适用于"For rent"房源。', + 'Total floor area (sqm)': + '在能源性能证书(EPC)评估期间测量的总可用建筑面积(平方米)。包括所有可居住房间,但不含车库、附属建筑和外部区域。', + 'Number of bedrooms & living rooms': + 'EPC中记录的可居住房间总数(卧室加客厅)。厨房和浴室通常不计入,除非面积足够大可算作可居住房间。', + Bedrooms: '在线房产房源中所列的卧室数量。仅适用于在线房源(出售和出租);历史销售记录为空。', + Bathrooms: '在线房产房源中所列的浴室数量。仅适用于在线房源(出售和出租);历史销售记录为空。', + 'Construction year': + '根据EPC中的建造年代段(例如"1930-1949")取中间值推算。对于年代段跨越数十年的老建筑,精度较低。', + 'Date of last transaction': + '来自英国土地注册局价格数据中该房产最近一次成交的记录日期。数据中以日期时间格式存储;在筛选和图表中转换为小数年份。', + 'Listing date': + '该房产房源首次出现在在线房产平台上的日期。以日期时间格式存储;筛选时转换为小数年份。仅适用于在线房源。', + 'Former council house': + '来自EPC数据中的TENURE字段。若该房产的任何一份EPC证书将产权记录为社会租赁,则表明该房产在该次评估时为政府或住房协会存量房。通过Right to Buy等方式出售后的房产仍保留此标记。', + 'Current energy rating': + '来自EPC的当前能源效率等级。从A(最高效)到G(最低效)。基于每平方米建筑面积的能源使用量。', + 'Potential energy rating': + '若实施EPC报告中建议的所有具有成本效益的改进措施后,该房产的潜在能源效率等级。从A(最高效)到G(最低效)。', + 'Interior height (m)': + 'EPC评估期间记录的平均室内净高(米)。通过将室内总容积除以总建筑面积计算得出。', + 'Distance to nearest train or tube station (km)': + '从邮政编码到最近铁路站或地铁/城铁/轻轨站的直线距离(km)。', + 'Good+ primary schools within 2km': + '2km范围内Ofsted评级为"Good"或"Outstanding"的公立小学数量。尚未接受评估的学校不计入。', + 'Good+ secondary schools within 2km': + '2km范围内Ofsted评级为"Good"或"Outstanding"的公立中学数量。尚未接受评估的学校不计入。', + 'Good+ primary schools within 5km': + '5km范围内Ofsted评级为"Good"或"Outstanding"的公立小学数量。尚未接受评估的学校不计入。', + 'Good+ secondary schools within 5km': + '5km范围内Ofsted评级为"Good"或"Outstanding"的公立中学数量。尚未接受评估的学校不计入。', + 'Education, Skills and Training Score': + '来自英格兰剥夺指数(取反后越高越好)。涵盖学校成绩、高等教育入学率、成人学历和英语水平。分数越高表示剥夺程度越低。', + 'Income Score (rate)': + '来自英格兰剥夺指数(取反后越高越好)。数值越高表示收入剥夺程度越低。基于收入支持、基于收入的求职者津贴、基于收入的就业与支持津贴、养老金补贴、工作税收抵免和子女税收抵免、普惠信用以及寻求庇护者等数据。', + 'Employment Score (rate)': + '来自英格兰剥夺指数(取反后越高越好)。数值越高表示就业剥夺程度越低。基于求职者津贴、就业与支持津贴、丧失劳动能力津贴、严重残疾津贴、护理者津贴申领者及相关普惠信用申领者等数据。', + 'Health Deprivation and Disability Score': + '来自英格兰剥夺指数(取反后越高越好)。分数越高表示过早死亡风险越低、生活质量越好。来源于潜在寿命损失年、比较疾病和残疾率、急性发病率以及情绪和焦虑障碍等指标。', + 'Living Environment Score': + '来自英格兰剥夺指数(取反后越高越好)。综合住房质量(状况、中央供暖)和室外环境(空气质量、道路安全)。分数越高表示居住环境越好。', + 'Indoors Sub-domain Score': + '来自英格兰剥夺指数的居住环境领域(取反后越高越好)。衡量住房存量质量:中央供暖覆盖率、住房状况以及Decent Homes标准。分数越高表示住房条件越好。', + 'Outdoors Sub-domain Score': + '来自英格兰剥夺指数的居住环境领域(取反后越高越好)。通过空气质量指标以及涉及行人和骑行者的道路交通事故伤亡人数衡量室外生活环境质量。分数越高表示室外环境越好。', + 'Serious crime per 1k residents (avg/yr)': + 'LSOA内每1,000名常住居民每年发生的暴力、抢劫、入室盗窃和持有武器犯罪数量。使用police.uk街道级犯罪数据(2023-2025年)和Census 2021人口数据。按人口密度标准化,便于不同规模地区之间的比较。', + 'Minor crime per 1k residents (avg/yr)': + 'LSOA内每1,000名常住居民每年发生的反社会行为、商店行窃、自行车盗窃及其他较轻微犯罪数量。使用police.uk街道级犯罪数据(2023-2025年)和Census 2021人口数据。按人口密度标准化,便于不同规模地区之间的比较。', + 'Serious crime (avg/yr)': + '来自police.uk街道级犯罪数据(2023-2025年)的LSOA内每年暴力、抢劫、入室盗窃和持有武器犯罪总和。提供单一的严重犯罪指标。', + 'Minor crime (avg/yr)': + '来自police.uk街道级犯罪数据(2023-2025年)的LSOA内每年反社会行为、商店行窃、自行车盗窃及其他较轻微犯罪总和。提供单一的轻微犯罪指标。', + 'Violence and sexual offences (avg/yr)': + 'LSOA内每年暴力和性犯罪的平均数量,来自police.uk街道级犯罪数据(2023-2025年)。包括攻击、骚扰和性犯罪。', + 'Burglary (avg/yr)': + 'LSOA内每年入室盗窃的平均数量,来自police.uk街道级犯罪数据(2023-2025年)。包括住宅和商业入室盗窃。', + 'Robbery (avg/yr)': + 'LSOA内每年抢劫案的平均数量,来自police.uk街道级犯罪数据(2023-2025年)。抢劫涉及以暴力或威胁手段实施的盗窃。', + 'Vehicle crime (avg/yr)': + 'LSOA内每年车辆犯罪事件的平均数量,来自police.uk街道级犯罪数据(2023-2025年)。包括盗窃车辆及从车辆内盗窃。', + 'Anti-social behaviour (avg/yr)': + 'LSOA内每年反社会行为事件的平均数量,来自police.uk街道级犯罪数据(2023-2025年)。包括滋扰、环境和个人反社会行为。', + 'Criminal damage and arson (avg/yr)': + 'LSOA内每年刑事损毁和纵火事件的平均数量,来自police.uk街道级犯罪数据(2023-2025年)。', + 'Other theft (avg/yr)': + 'LSOA内每年"其他盗窃"案的平均数量,来自police.uk街道级犯罪数据(2023-2025年)。包括未被归类为入室盗窃、车辆犯罪、商店行窃或自行车盗窃的盗窃行为。', + 'Theft from the person (avg/yr)': + 'LSOA内每年针对人身盗窃案的平均数量,来自police.uk街道级犯罪数据(2023-2025年)。包括扒窃和未使用暴力的抢包行为。', + 'Shoplifting (avg/yr)': + 'LSOA内每年商店行窃案的平均数量,来自police.uk街道级犯罪数据(2023-2025年)。', + 'Bicycle theft (avg/yr)': + 'LSOA内每年自行车盗窃案的平均数量,来自police.uk街道级犯罪数据(2023-2025年)。', + 'Drugs (avg/yr)': + 'LSOA内每年毒品犯罪的平均数量,来自police.uk街道级犯罪数据(2023-2025年)。包括持有和贩运毒品犯罪。', + 'Possession of weapons (avg/yr)': + 'LSOA内每年持有武器犯罪的平均数量,来自police.uk街道级犯罪数据(2023-2025年)。', + 'Public order (avg/yr)': + 'LSOA内每年公共秩序违法行为的平均数量,来自police.uk街道级犯罪数据(2023-2025年)。包括引起他人恐惧、惊扰或困扰的行为。', + 'Other crime (avg/yr)': + 'LSOA内每年其他犯罪的平均数量,来自police.uk街道级犯罪数据(2023-2025年)。此类别涵盖未在其他分类中列出的犯罪行为。', + 'Median age': + '来自2021年Census(TS007A)。通过对五岁年龄段人口数进行线性插值计算得出的LSOA常住居民年龄中位数。年轻人口集中的地区往往是城市、大学城或家庭聚居地;年龄中位数较高的地区多见于农村和沿海地区。', + '% White': + '来自2021年Census。地方政府人口中认同为白人(英格兰人、威尔士人、苏格兰人、北爱尔兰人、英国人、爱尔兰人、吉普赛人或爱尔兰旅行者、罗姆人或其他白人背景)的百分比。', + '% South Asian': + '来自2021年Census。地方政府人口中认同为印度人、巴基斯坦人、孟加拉国人或其他亚洲背景的百分比。', + '% Black': '来自2021年Census。地方政府人口中认同为黑人、英国黑人、加勒比人或非洲人的百分比。', + '% East Asian': '来自2021年Census。地方政府人口中认同为华人的百分比。', + '% Mixed': + '来自2021年Census。地方政府人口中认同为��血或多种族群体(白人与黑人加勒比裔、白人与黑人非洲裔、白人与亚洲裔,或其他混血或多种族背景)的百分比。', + '% Other': + '来自2021年Census。地方政府人口中认同为其他族裔群体(阿拉伯人或其他未被主要类别涵盖的族裔)的百分比。', + 'Distance to nearest park (km)': + '从邮政编码到最近公园入口的直线距离(km)。涵盖公共公园、花园、运动���和游乐场地。使用OS Open Greenspace数据集中的出入口位置,因此紧邻大型公园的房产可正确显示较短距离。', + 'Number of parks within 2km': + '以房产邮政编码中心点为圆心,2km半径内至少有一个入口的公共公园、花园、运动场和游乐场地数量。来源于OS Open Greenspace数据集(英国地形测量局),使用公园入口位置进行精确近距离匹配。', + 'Number of restaurants within 2km': + '邮政编码2km范围内的餐厅、咖啡馆和餐饮场所数量。来源于OpenStreetMap。', + 'Number of grocery shops and supermarkets within 2km': + '以房产邮政编码中心点为圆心,2km半径内的超市、便利店和其他杂货店数量。来源于OpenStreetMap POI数据。', + 'Noise (dB)': + '来自Defra战略噪声图第4轮(2022年)的道路噪声水平,单位为分贝(Lden,24小时加权平均值)。在地面以上4m、10m网格间距处建模。一般而言,超过约55 dB可明显感知;超过约70 dB被世卫组织认定为有害。', + 'Max available download speed (Mbps)': + '来自Ofcom Connected Nations 2025的任意运营商可提供的最大固定宽带下载速度。代表理论最大值,而非实际达到的速度。10 Mbps为基础级,30为超快级,100+为极速级,1000为千兆级。', + }, + hu: { + 'Listing status': + "Az ingatlan bejegyzés forrását jelzi: 'Korábbi adásvétel' az HM Land Registry Price Paid adatokból, 'Eladó' az aktuális online vételi hirdetésekből, vagy 'Kiadó' az aktuális online bérleti hirdetésekből.", + 'Property type': + 'Az HM Land Registry Price Paid adatokból és EPC tanúsítványokból. Különálló, Ikerház, Sorház (minden sorház altípust tartalmaz), Lakás/Maisonette, vagy Egyéb (bungaló, mobilház stb.).', + 'Leasehold/Freehold': + 'Az HM Land Registry Price Paid adatokból. A Freehold azt jelenti, hogy az épület és a telek is az Ön tulajdona. A Leasehold azt jelenti, hogy az épület az Ön tulajdona, de a telek nem: a telektulajdonostól meghatározott számú évre szóló bérleti jogot kapott.', + 'Last known price': + 'Az ingatlan utolsó rögzített adásvételi ára az HM Land Registry Price Paid adatokból. Az angliai lakóingatlan-értékesítésekre vonatkozik. Lehet, hogy évekkel ezelőtti adat, ha az ingatlan nem kelt el a közelmúltban.', + 'Estimated current price': + 'Az utolsó adásvételi áron alapul, amelyet az idő múlásával bekövetkezett helyi árváltozásokhoz igazítottak egy ismételt értékesítési index segítségével (irányítószám-szektor és ingatlan típusa szerint nyomon követve). Ha az EPC-adatokból az értékesítés utáni felújítás észlelhető, felújítási prémium kerül hozzáadásra. A közelmúltbeli adásvételek közel lesznek az eredeti árhoz; a régebbi adásvételeket jobban korrigálják.', + 'Asking price': + "Az online ingatlanportálokon hirdetett kért ár. Csak az 'Eladó' hirdetéseknél érhető el.", + 'Price per sqm': + 'Az utolsó ismert adásvételi árat az EPC tanúsítványból származó teljes alapterülettel elosztva számítják ki. Hasznos a különböző méretű ingatlanok értékének összehasonlításához. Csak akkor elérhető, ha mind az ár, mind az alapterület adatai rendelkezésre állnak.', + 'Est. price per sqm': + 'Az inflációval korrigált becsült aktuális árat (beleértve az esetleges felújítási prémiumot) az EPC tanúsítványból származó teljes alapterülettel elosztva számítják ki. Naprakészebb ár/terület összehasonlítást nyújt, mint a korábbi adásvételi ár per sqm.', + 'Asking price per sqm': + 'A megadott kért árat az összes alapterülettel elosztva számítják ki. Csak azon ingatlanokra vonatkozik, amelyek jelenleg eladók, és amelyekről alapterület-adatok állnak rendelkezésre.', + 'Estimated monthly rent': + 'Az ONS Magánbérleti Piaci Összefoglaló Statisztikákból (2022. október – 2023. szeptember) származó medián havi bérleti díj, helyi hatóság és hálószobák száma szerint párosítva. A Valuation Office Agency bérbeadási adatain alapul.', + 'Asking rent (monthly)': + "Az online ingatlanportálokon hirdetett bérleti díj, szükség esetén havi összegre átszámítva (pl. heti vagy éves hirdetések esetén). Csak a 'Kiadó' hirdetéseknél érhető el.", + 'Total floor area (sqm)': + 'Az Energy Performance Certificate felmérése során mért teljes hasznos alapterület négyzetméterben. Tartalmazza az összes lakható helyiséget, de kizárja a garázsokat, melléképületeket és külső területeket.', + 'Number of bedrooms & living rooms': + 'Az Energy Performance Certificate-ben rögzített összes lakható helyiség száma (hálószobák és nappali szobák összege). A konyhák és fürdőszobák jellemzően nem számítanak bele, kivéve, ha elég nagyok ahhoz, hogy lakható helyiségnek minősüljenek.', + Bedrooms: + 'Az online ingatlanhirdetésben meghirdetett hálószobák száma. Csak online hirdetéseknél (eladó és kiadó ingatlanok) kerül feltüntetésre; korábbi adásvételek esetén üres.', + Bathrooms: + 'Az online ingatlanhirdetésben meghirdetett fürdőszobák száma. Csak online hirdetéseknél (eladó és kiadó ingatlanok) kerül feltüntetésre; korábbi adásvételek esetén üres.', + 'Construction year': + "Az EPC-ben szereplő építési korszak alapján (pl. '1930–1949') a középértékkel becsülve. Régebbi épületeknél kevésbé pontos, ahol a korcsoport több évtizedet ölel fel.", + 'Date of last transaction': + 'Az ingatlan legutóbbi rögzített adásvételének dátuma az HM Land Registry Price Paid adatokból. Az adatokban dátum/idő formátumban tárolódik; szűréshez és diagramokhoz törtéves formátumra konvertálva.', + 'Listing date': + 'Az a dátum, amikor az ingatlanhirdetés először jelent meg az online ingatlanportálon. Dátum/idő formátumban tárolva; szűréshez törtéves formátumra konvertálva. Csak online hirdetéseknél kerül feltüntetésre.', + 'Former council house': + 'Az Energy Performance Certificate adatok TENURE mezőjéből származtatva. Ha az ingatlan bármely EPC tanúsítványa szociális bérlakásként rögzítette a bérleti jogviszonyt, ez azt jelzi, hogy az ingatlan az adott ellenőrzés idején önkormányzati vagy lakásszövetkezeti állomány volt. Azok az ingatlanok, amelyeket később értékesítettek (pl. Right to Buy útján), megőrzik ezt a jelzést.', + 'Current energy rating': + 'Az Energy Performance Certificate aktuális energiahatékonysági besorolása. A-tól (leghatékonyabb) G-ig (legkevésbé hatékony) terjed. Az ingatlan alapterületre vetített energiafelhasználásán alapul.', + 'Potential energy rating': + 'Az Energy Performance Certificate potenciális energiahatékonysági besorolása, amennyiben az EPC-jelentésben ajánlott összes költséghatékony fejlesztést elvégeznék. A-tól (leghatékonyabb) G-ig (legkevésbé hatékony) terjed.', + 'Interior height (m)': + 'Az Energy Performance Certificate felmérése során rögzített átlagos belső padló-mennyezet magasság méterben. A teljes belső térfogatot osztják a teljes alapterülettel.', + 'Distance to nearest train or tube station (km)': + 'Légvonalbeli távolság kilométerben az irányítószámtól a legközelebbi vasút- vagy metró-/városi vasút-/villamosmegállóig.', + 'Good+ primary schools within 2km': + '2 km-en belüli állami fenntartású általános iskolák, amelyek aktuális Ofsted besorolása Jó vagy Kiemelkedő. A még nem vizsgált iskolák ki vannak zárva.', + 'Good+ secondary schools within 2km': + '2 km-en belüli állami fenntartású középiskolák, amelyek aktuális Ofsted besorolása Jó vagy Kiemelkedő. A még nem vizsgált iskolák ki vannak zárva.', + 'Good+ primary schools within 5km': + '5 km-en belüli állami fenntartású általános iskolák, amelyek aktuális Ofsted besorolása Jó vagy Kiemelkedő. A még nem vizsgált iskolák ki vannak zárva.', + 'Good+ secondary schools within 5km': + '5 km-en belüli állami fenntartású középiskolák, amelyek aktuális Ofsted besorolása Jó vagy Kiemelkedő. A még nem vizsgált iskolák ki vannak zárva.', + 'Education, Skills and Training Score': + 'Az Angol Nélkülözési Indexekből (megfordítva, így magasabb = jobb). Az iskolai teljesítményt, a felsőoktatásba való bejutást, a felnőttkori képesítéseket és az angol nyelvi jártasságot foglalja magában. A magasabb pontszámok kisebb mértékű nélkülözést jeleznek.', + 'Income Score (rate)': + 'Az Angol Nélkülözési Indexekből (megfordítva, így magasabb = jobb). A magasabb értékek kisebb mértékű jövedelmi nélkülözést jeleznek. A jövedelempótló támogatás, jövedelemalapú Munkaügyi Segély, jövedelemalapú Foglalkoztatási és Támogatási Segély, Nyugdíjkiegészítés, Munkavállalói és Gyermekadókedvezmény, Univerzális Hitel és menedékkérők alapján.', + 'Employment Score (rate)': + 'Az Angol Nélkülözési Indexekből (megfordítva, így magasabb = jobb). A magasabb értékek kisebb mértékű foglalkoztatási nélkülözést jeleznek. A Munkaügyi Segély, Foglalkoztatási és Támogatási Segély, Munkaképtelenségi Juttatás, Súlyos Rokkantsági Pótlék, Gondozói Juttatás igénylői és a vonatkozó Univerzális Hitel igénylői alapján.', + 'Health Deprivation and Disability Score': + 'Az Angol Nélkülözési Indexekből (megfordítva, így magasabb = jobb). A magasabb pontszámok alacsonyabb korai halálozási kockázatot és jobb életminőséget jeleznek. Az elveszített potenciális életévekből, a komparatív betegségi és rokkantsági arányból, az akut morbiditásból, valamint a hangulati és szorongásos zavarokból vezethető le.', + 'Living Environment Score': + 'Az Angol Nélkülözési Indexekből (megfordítva, így magasabb = jobb). Ötvözi a lakásminőséget (állapot, gázfűtés) és a külső környezetet (levegőminőség, közlekedésbiztonság). A magasabb pontszámok jobb lakókörnyezetet jeleznek.', + 'Indoors Sub-domain Score': + 'Az Angol Nélkülözési Indexek Lakókörnyezet tartományából (megfordítva, így magasabb = jobb). A lakásállomány minőségét méri: gázfűtés rendelkezésre állása, lakásállapot és Decent Homes szabványok. A magasabb pontszámok jobb lakáskörülményeket jeleznek.', + 'Outdoors Sub-domain Score': + 'Az Angol Nélkülözési Indexek Lakókörnyezet tartományából (megfordítva, így magasabb = jobb). A külső lakókörnyezet minőségét méri a levegőminőségi mutatók és a gyalogosokat, kerékpárosokat érintő közúti közlekedési baleseti áldozatok alapján. A magasabb pontszámok jobb külső környezetet jeleznek.', + 'Serious crime per 1k residents (avg/yr)': + 'Erőszakos bűncselekmények, rablás, betörés és fegyverbirtoklás 1 000 szokásos lakóra vetítve évente az LSOA-ban. A police.uk utcai szintű bűnügyi adatait (2023–2025) és a Census 2021 népességszámait használja. Normalizálja a népsűrűséget, így a területek mérettől függetlenül összehasonlíthatók.', + 'Minor crime per 1k residents (avg/yr)': + 'Antiszociális magatartás, boltlopás, kerékpárlopás és egyéb kisebb súlyosságú bűncselekmények 1 000 szokásos lakóra vetítve évente az LSOA-ban. A police.uk utcai szintű bűnügyi adatait (2023–2025) és a Census 2021 népességszámait használja. Normalizálja a népsűrűséget, így a területek mérettől függetlenül összehasonlíthatók.', + 'Serious crime (avg/yr)': + 'Az erőszakos bűncselekmények, rablás, betörés és fegyverbirtoklás éves összege az LSOA-ban, a police.uk utcai szintű bűnügyi adataiból (2023–2025). Egyetlen súlyos bűnözési mutatót ad.', + 'Minor crime (avg/yr)': + 'Az antiszociális magatartás, boltlopás, kerékpárlopás és egyéb kisebb súlyosságú bűncselekmények éves összege az LSOA-ban, a police.uk utcai szintű bűnügyi adataiból (2023–2025). Egyetlen kisebb bűnözési mutatót ad.', + 'Violence and sexual offences (avg/yr)': + 'Az erőszakos és szexuális bűncselekmények átlagos éves száma az LSOA-ban, a police.uk utcai szintű bűnügyi adataiból (2023–2025). Magában foglalja a testi sértést, zaklatást és szexuális bűncselekményeket.', + 'Burglary (avg/yr)': + 'A betörések átlagos éves száma az LSOA-ban, a police.uk utcai szintű bűnügyi adataiból (2023–2025). Magában foglalja a lakó- és kereskedelmi célú betöréseket.', + 'Robbery (avg/yr)': + 'A rablások átlagos éves száma az LSOA-ban, a police.uk utcai szintű bűnügyi adataiból (2023–2025). A rablás erővel vagy erőszakkal fenyegetéssel járó lopást jelent.', + 'Vehicle crime (avg/yr)': + 'A járművel kapcsolatos bűncselekmények átlagos éves száma az LSOA-ban, a police.uk utcai szintű bűnügyi adataiból (2023–2025). Magában foglalja a járművek ellopását és a járművekből való lopást.', + 'Anti-social behaviour (avg/yr)': + 'Az antiszociális magatartási esetek átlagos éves száma az LSOA-ban, a police.uk utcai szintű bűnügyi adataiból (2023–2025). Magában foglalja a zavarást, környezeti és személyes antiszociális magatartást.', + 'Criminal damage and arson (avg/yr)': + 'A rongálás és gyújtogatás átlagos éves száma az LSOA-ban, a police.uk utcai szintű bűnügyi adataiból (2023–2025).', + 'Other theft (avg/yr)': + "Az 'egyéb lopás' kategóriájú bűncselekmények átlagos éves száma az LSOA-ban, a police.uk utcai szintű bűnügyi adataiból (2023–2025). Magában foglalja a betörés, járműves bűncselekmény, boltlopás vagy kerékpárlopás alá nem sorolt lopásokat.", + 'Theft from the person (avg/yr)': + 'A személytől való lopás átlagos éves száma az LSOA-ban, a police.uk utcai szintű bűnügyi adataiból (2023–2025). Magában foglalja a zsebtolvajlást és erő nélküli táskavágást.', + 'Shoplifting (avg/yr)': + 'A boltlopások átlagos éves száma az LSOA-ban, a police.uk utcai szintű bűnügyi adataiból (2023–2025).', + 'Bicycle theft (avg/yr)': + 'A kerékpárlopások átlagos éves száma az LSOA-ban, a police.uk utcai szintű bűnügyi adataiból (2023–2025).', + 'Drugs (avg/yr)': + 'A kábítószer-bűncselekmények átlagos éves száma az LSOA-ban, a police.uk utcai szintű bűnügyi adataiból (2023–2025). Magában foglalja a birtoklási és terjesztési bűncselekményeket.', + 'Possession of weapons (avg/yr)': + 'A fegyverbirtoklási bűncselekmények átlagos éves száma az LSOA-ban, a police.uk utcai szintű bűnügyi adataiból (2023–2025).', + 'Public order (avg/yr)': + 'A közrend elleni bűncselekmények átlagos éves száma az LSOA-ban, a police.uk utcai szintű bűnügyi adataiból (2023–2025). Magában foglalja a félelemkeltést, riasztást vagy szorongást okozó cselekményeket.', + 'Other crime (avg/yr)': + 'Az egyéb bűncselekmények átlagos éves száma az LSOA-ban, a police.uk utcai szintű bűnügyi adataiból (2023–2025). Gyűjtőkategória azoknak a bűncselekményeknek, amelyek máshol nem kerülnek besorolásra.', + 'Median age': + 'A 2021-es Census alapján (TS007A). Az LSOA szokásos lakóinak medián életkora, ötéves korcsoport-számlálásokból lineáris interpolációval számítva. A fiatalabb népességű területek jellemzően városiak, egyetemi városok vagy több családot vonzanak; az idősebb medián értékek jellemzően vidéki és tengerparti területekre jellemzők.', + '% White': + 'A 2021-es Census alapján. A helyi hatóság területén fehérként (angol, walesi, skót, észak-ír, brit, ír, cigány vagy ír vándor, roma, vagy bármely más fehér háttér) azonosított népesség százaléka.', + '% South Asian': + 'A 2021-es Census alapján. A helyi hatóság területén indiai, pakisztáni, bangladesi vagy bármely más ázsiai háttérként azonosított népesség százaléka.', + '% Black': + 'A 2021-es Census alapján. A helyi hatóság területén fekete, brit fekete, karibi vagy afrikai háttérként azonosított népesség százaléka.', + '% East Asian': + 'A 2021-es Census alapján. A helyi hatóság területén kínaiként azonosított népesség százaléka.', + '% Mixed': + 'A 2021-es Census alapján. A helyi hatóság területén vegyes vagy többes etnikai csoportként (fehér és fekete karibi, fehér és fekete afrikai, fehér és ázsiai, vagy bármely más vegyes vagy többes háttér) azonosított népesség százaléka.', + '% Other': + 'A 2021-es Census alapján. A helyi hatóság területén egyéb etnikai csoportként (arab vagy bármely más, a főkategóriák által nem lefedett etnikai csoport) azonosított népesség százaléka.', + 'Distance to nearest park (km)': + 'Légvonalbeli távolság kilométerben az irányítószámtól a legközelebbi park bejáratáig. Magában foglalja a közparkokat, kerteket, játszótereket és szabadidős területeket. Az OS Open Greenspace adatkészlet hozzáférési pont helyszíneit használja, így a nagy park szomszédságában lévő ingatlanok helyesen rövid távolságot mutatnak.', + 'Number of parks within 2km': + 'A közparkok, kertek, játszóterek és szabadidős területek száma, amelyeknek legalább egy bejárata van az ingatlan irányítószám centroidjától számított 2 km-es körzetben. Az OS Open Greenspace adatkészletből (Ordnance Survey) származik, park bejárati helyszíneket használva a pontos közelségi egyeztetéshez.', + 'Number of restaurants within 2km': + 'Az ingatlan irányítószámjától 2 km-en belüli éttermek, kávézók és vendéglátóhelyek. Forrás: OpenStreetMap.', + 'Number of grocery shops and supermarkets within 2km': + 'Az ingatlan irányítószám centroidjától számított 2 km-es körzetben lévő szupermarketek, kisboltok és egyéb élelmiszerboltok száma. Az OpenStreetMap POI-adatokból származtatva.', + 'Noise (dB)': + 'Közúti zajszint decibel (Lden, 24 órás súlyozott átlag) értékben, a Defra Stratégiai Zajtérképezés 4. fordulójából (2022). 4 m magasságban, 10 m-es rácson modellezve. ~55 dB felett általában érzékelhető; ~70 dB felett az WHO károsnak minősíti.', + 'Max available download speed (Mbps)': + 'Bármely szolgáltatótól elérhető maximális rögzített szélessávú letöltési sebesség, az Ofcom Connected Nations 2025 adataiból. Az elméleti maximumot jelöli, nem a valós sebességet. 10 Mbps = alapszintű, 30 = szupergyors, 100+ = ultragyors, 1000 = gigabites.', + }, +}; diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts index c9fe142..05e5e12 100644 --- a/frontend/src/i18n/index.ts +++ b/frontend/src/i18n/index.ts @@ -7,11 +7,11 @@ import hu from './locales/hu'; import zh from './locales/zh'; export const SUPPORTED_LANGUAGES = [ - { code: 'en', label: 'English', flag: '\uD83C\uDDEC\uD83C\uDDE7' }, - { code: 'fr', label: 'Fran\u00E7ais', flag: '\uD83C\uDDEB\uD83C\uDDF7' }, - { code: 'de', label: 'Deutsch', flag: '\uD83C\uDDE9\uD83C\uDDEA' }, - { code: 'hu', label: 'Magyar', flag: '\uD83C\uDDED\uD83C\uDDFA' }, - { code: 'zh', label: '\u4E2D\u6587', flag: '\uD83C\uDDE8\uD83C\uDDF3' }, + { code: 'en', label: 'English', flag: '\uD83C\uDDEC\uD83C\uDDE7' }, + { code: 'fr', label: 'Fran\u00E7ais', flag: '\uD83C\uDDEB\uD83C\uDDF7' }, + { code: 'de', label: 'Deutsch', flag: '\uD83C\uDDE9\uD83C\uDDEA' }, + { code: 'hu', label: 'Magyar', flag: '\uD83C\uDDED\uD83C\uDDFA' }, + { code: 'zh', label: '\u4E2D\u6587', flag: '\uD83C\uDDE8\uD83C\uDDF3' }, ] as const; export type LanguageCode = (typeof SUPPORTED_LANGUAGES)[number]['code']; @@ -19,37 +19,37 @@ export type LanguageCode = (typeof SUPPORTED_LANGUAGES)[number]['code']; const supportedCodes: Set = new Set(SUPPORTED_LANGUAGES.map((l) => l.code)); function detectLanguage(): string { - // 1. Explicit user choice (persisted from the language dropdown) - const stored = localStorage.getItem('language'); - if (stored && supportedCodes.has(stored)) return stored; + // 1. Explicit user choice (persisted from the language dropdown) + const stored = localStorage.getItem('language'); + if (stored && supportedCodes.has(stored)) return stored; - // 2. Browser preference (navigator.languages falls back to navigator.language) - for (const tag of navigator.languages ?? [navigator.language]) { - // Match full tag first (e.g. "zh-CN" → "zh"), then just the prefix - const lower = tag.toLowerCase(); - if (supportedCodes.has(lower)) return lower; - const prefix = lower.split('-')[0]; - if (supportedCodes.has(prefix)) return prefix; - } + // 2. Browser preference (navigator.languages falls back to navigator.language) + for (const tag of navigator.languages ?? [navigator.language]) { + // Match full tag first (e.g. "zh-CN" → "zh"), then just the prefix + const lower = tag.toLowerCase(); + if (supportedCodes.has(lower)) return lower; + const prefix = lower.split('-')[0]; + if (supportedCodes.has(prefix)) return prefix; + } - return 'en'; + return 'en'; } const initialLang = detectLanguage(); i18n.use(initReactI18next).init({ - resources: { - en: { translation: en }, - fr: { translation: fr }, - de: { translation: de }, - hu: { translation: hu }, - zh: { translation: zh }, - }, - lng: initialLang, - fallbackLng: 'en', - interpolation: { - escapeValue: false, // React already escapes - }, + resources: { + en: { translation: en }, + fr: { translation: fr }, + de: { translation: de }, + hu: { translation: hu }, + zh: { translation: zh }, + }, + lng: initialLang, + fallbackLng: 'en', + interpolation: { + escapeValue: false, // React already escapes + }, }); /** @@ -57,8 +57,8 @@ i18n.use(initReactI18next).init({ * Bypasses the strict type checking on t() for dynamic key construction. */ export function tDynamic(key: string): string { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (i18n.t as any)(key); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (i18n.t as any)(key); } export default i18n; diff --git a/frontend/src/i18n/server.ts b/frontend/src/i18n/server.ts index 01e4b94..141f7d8 100644 --- a/frontend/src/i18n/server.ts +++ b/frontend/src/i18n/server.ts @@ -11,5 +11,5 @@ export function ts(value: string): string { return typeof result === 'string' ? result : value; } -// Re-export tsDesc from descriptions.ts for convenience -export { tsDesc } from './descriptions'; +// Re-export tsDesc and tsDetail from descriptions.ts for convenience +export { tsDesc, tsDetail } from './descriptions'; diff --git a/frontend/src/lib/PieHexExtension.ts b/frontend/src/lib/PieHexExtension.ts new file mode 100644 index 0000000..77cac25 --- /dev/null +++ b/frontend/src/lib/PieHexExtension.ts @@ -0,0 +1,124 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { LayerExtension } from '@deck.gl/core'; +import { ENUM_PALETTE } from './consts'; + +/** + * LayerExtension that turns polygon fills into pie charts. + * Injects a fragment shader that computes angle from each fragment's position + * to the polygon centroid, then picks a slice color from the enum palette. + * + * Works with H3HexagonLayer (hex fills) and GeoJsonLayer (postcode fills). + * Only activates on SolidPolygonLayer sublayers (fill), not PathLayer (stroke). + * + * Required layer props when this extension is active: + * getCenter: (d) => [lon, lat] — polygon centroid in world coordinates + * getRatios0: (d) => number[4] — pie ratios for slices 0-3 + * getRatios1: (d) => number[4] — pie ratios for slices 4-7 + * getRatios2: (d) => number[2] — pie ratios for slices 8-9 + */ + +// Build palette as GLSL vec3 constants (normalized 0-1) +const PALETTE_GLSL = ENUM_PALETTE.map( + (c) => + `vec3(${(c[0] / 255).toFixed(4)}, ${(c[1] / 255).toFixed(4)}, ${(c[2] / 255).toFixed(4)})` +).join(',\n '); + +export class PieHexExtension extends LayerExtension { + static extensionName = 'PieHexExtension'; + + isEnabled(layer: any): boolean { + // Only apply to fill sublayers (SolidPolygonLayer), not stroke (PathLayer) + return layer.id.endsWith('-fill'); + } + + getShaders(extension: any): any { + if (!extension.isEnabled(this)) return null; + return { + modules: [ + { + name: 'pieHex', + inject: { + 'vs:#decl': `\ +in vec2 instancePieCenter; +in vec4 instanceRatios0; +in vec4 instanceRatios1; +in vec2 instanceRatios2; +out vec2 vPieCenter; +out vec2 vPieFragPos; +out vec4 vRatios0; +out vec4 vRatios1; +out vec2 vRatios2;`, + 'vs:#main-end': `\ +vPieCenter = project_position(vec3(instancePieCenter, 0.0)).xy; +vPieFragPos = geometry.position.xy; +vRatios0 = instanceRatios0; +vRatios1 = instanceRatios1; +vRatios2 = instanceRatios2;`, + 'fs:#decl': `\ +in vec2 vPieCenter; +in vec2 vPieFragPos; +in vec4 vRatios0; +in vec4 vRatios1; +in vec2 vRatios2; +const vec3 pieColors[10] = vec3[10]( + ${PALETTE_GLSL} +);`, + 'fs:DECKGL_FILTER_COLOR': `\ +{ + vec2 delta = vPieFragPos - vPieCenter; + float angle = atan(delta.x, -delta.y) / (2.0 * 3.14159265) + 0.5; + + float ratios[10]; + ratios[0] = vRatios0.x; ratios[1] = vRatios0.y; + ratios[2] = vRatios0.z; ratios[3] = vRatios0.w; + ratios[4] = vRatios1.x; ratios[5] = vRatios1.y; + ratios[6] = vRatios1.z; ratios[7] = vRatios1.w; + ratios[8] = vRatios2.x; ratios[9] = vRatios2.y; + + float cumulative = 0.0; + vec3 sliceColor = pieColors[0]; + for (int i = 0; i < 10; i++) { + cumulative += ratios[i]; + if (angle < cumulative) { + sliceColor = pieColors[i]; + break; + } + } + + color = vec4(sliceColor, 1.0); +}`, + }, + uniformTypes: {}, + }, + ], + }; + } + + initializeState(this: any, _context: any, extension: any): void { + if (!extension.isEnabled(this)) return; + const am = this.getAttributeManager(); + if (!am) return; + am.addInstanced({ + instancePieCenter: { + size: 2, + type: 'float32', + accessor: 'getCenter', + }, + instanceRatios0: { + size: 4, + type: 'float32', + accessor: 'getRatios0', + }, + instanceRatios1: { + size: 4, + type: 'float32', + accessor: 'getRatios1', + }, + instanceRatios2: { + size: 2, + type: 'float32', + accessor: 'getRatios2', + }, + }); + } +} diff --git a/frontend/src/lib/format.ts b/frontend/src/lib/format.ts index 8fbc38b..496410d 100644 --- a/frontend/src/lib/format.ts +++ b/frontend/src/lib/format.ts @@ -46,6 +46,7 @@ export function parseInputValue( export function formatDuration(d: string): string { if (d === 'F' || d === 'L') { // These are server enum values — translate via ts() + // eslint-disable-next-line @typescript-eslint/no-var-requires const { ts } = require('../i18n/server') as { ts: (v: string) => string }; if (d === 'F') return ts('Freehold'); return ts('Leasehold'); @@ -86,7 +87,10 @@ export function formatNumber(value: number | undefined, decimals = 0): string { } export function formatRelativeTime(isoDate: string): string { - const i18n = require('../i18n').default as { t: (key: string, opts?: Record) => string }; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const i18n = require('../i18n').default as { + t: (key: string, opts?: Record) => string; + }; const now = Date.now(); const then = new Date(isoDate).getTime(); const diffMs = now - then; diff --git a/frontend/src/lib/map-utils.ts b/frontend/src/lib/map-utils.ts index 805a717..884bdd4 100644 --- a/frontend/src/lib/map-utils.ts +++ b/frontend/src/lib/map-utils.ts @@ -232,7 +232,7 @@ export function getFeatureFillColor( } } - // Discrete coloring for enum features + // Discrete coloring for enum features (used as base; PieHexExtension overrides when active) if (enumCount > 0) { const rgb = enumIndexToColor(Math.round(value as number)); return [...rgb, alpha] as [number, number, number, number]; diff --git a/frontend/src/lib/url-state.ts b/frontend/src/lib/url-state.ts index b6ee2cd..3909cbe 100644 --- a/frontend/src/lib/url-state.ts +++ b/frontend/src/lib/url-state.ts @@ -160,7 +160,11 @@ export function stateToParams( } export function summarizeParams(queryString: string): string { - const i18n = require('../i18n').default as { t: (key: string, opts?: Record) => string }; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const i18n = require('../i18n').default as { + t: (key: string, opts?: Record) => string; + }; + // eslint-disable-next-line @typescript-eslint/no-var-requires const { ts } = require('../i18n/server') as { ts: (v: string) => string }; const params = new URLSearchParams(queryString); const parts: string[] = []; diff --git a/server-rs/logs/server.log.2026-04-04 b/server-rs/logs/server.log.2026-04-04 new file mode 100644 index 0000000..b0df734 --- /dev/null +++ b/server-rs/logs/server.log.2026-04-04 @@ -0,0 +1,1017 @@ +2026-04-04T09:28:02.133890Z INFO property_map_server: Prometheus metrics initialized +2026-04-04T09:28:02.134037Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet +2026-04-04T09:28:02.134042Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet" +2026-04-04T09:28:02.190205Z INFO property_map_server::data::property: Postcode features loaded rows=1262364 +2026-04-04T09:28:02.190214Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet" +2026-04-04T09:28:04.392037Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203393 +2026-04-04T09:28:04.392047Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet" +2026-04-04T09:28:04.770425Z INFO property_map_server::data::property: buy listings joined rows=1060501 +2026-04-04T09:28:04.770442Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet" +2026-04-04T09:28:04.875727Z INFO property_map_server::data::property: rent listings joined rows=266585 +2026-04-04T09:28:04.875738Z INFO property_map_server::data::property: Concatenating all data sources +2026-04-04T09:28:09.805925Z INFO property_map_server::data::property: All data sources combined properties=15203393 buy_listings=1060501 rent_listings=266585 total=16530479 +2026-04-04T09:28:09.806015Z INFO property_map_server::data::property: Feature columns from config numeric=58 enums=7 total=65 +2026-04-04T09:28:11.207110Z INFO property_map_server::data::property: Combined data selected rows=16530479 +2026-04-04T09:28:11.362341Z INFO property_map_server::data::property: Extracting numeric feature columns +2026-04-04T09:28:11.727944Z INFO property_map_server::data::property: Computing histograms for numeric features +2026-04-04T09:28:12.922797Z INFO property_map_server::data::property: Extracting string columns +2026-04-04T09:28:15.269208Z INFO property_map_server::data::property: Building enum features +2026-04-04T09:28:16.601348Z INFO property_map_server::data::property: Extracting renovation history +2026-04-04T09:28:18.638703Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829784 +2026-04-04T09:28:18.638711Z INFO property_map_server::data::property: Extracting listing features +2026-04-04T09:28:19.310875Z INFO property_map_server::data::property: Listing features extracted properties_with_features=527541 +2026-04-04T09:28:19.310886Z INFO property_map_server::data::property: Sorting rows by spatial locality +2026-04-04T09:28:20.259288Z INFO property_map_server::data::property: Building interned strings +2026-04-04T09:28:26.626582Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted, quantized to u16) +2026-04-04T09:28:29.216545Z INFO property_map_server::data::property: Data loading complete +2026-04-04T09:28:30.912405Z INFO property_map_server: Property data loaded rows=16530479 features=65 enums=7 +2026-04-04T09:28:30.912414Z INFO property_map_server: Building spatial grid index (0.01° cells) +2026-04-04T09:28:31.009464Z INFO property_map_server: Precomputing H3 cells at resolution 12 +2026-04-04T09:28:31.009472Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12 +2026-04-04T09:28:31.412921Z INFO property_map_server::data::property: H3 precomputation complete (16530479 cells) +2026-04-04T09:28:31.412990Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet +2026-04-04T09:28:31.413014Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"... +2026-04-04T09:28:31.431682Z INFO property_map_server::data::poi: Loaded 550611 POIs +2026-04-04T09:28:31.520555Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71 +2026-04-04T09:28:31.521010Z INFO property_map_server::data::poi: POI data loading complete. +2026-04-04T09:28:31.550742Z INFO property_map_server: POI data loaded pois=550611 +2026-04-04T09:28:31.550748Z INFO property_map_server: Building POI spatial grid index +2026-04-04T09:28:31.555128Z INFO property_map_server: Loading place data from /app/data/places.parquet +2026-04-04T09:28:31.555134Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"... +2026-04-04T09:28:31.555637Z INFO property_map_server::data::places: Loaded 3474 places +2026-04-04T09:28:31.556345Z INFO property_map_server::data::places: Place data loaded places=3474 types=2 with_population=71 with_city=3392 +2026-04-04T09:28:31.556396Z INFO property_map_server: Place data loaded places=3474 +2026-04-04T09:28:31.556403Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries +2026-04-04T09:28:31.556406Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries" +2026-04-04T09:28:31.557177Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361 +2026-04-04T09:28:38.911450Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140 +2026-04-04T09:28:39.131253Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140 +2026-04-04T09:28:39.267705Z INFO property_map_server::data::postcodes: Outcode data derived from postcodes outcodes=2361 +2026-04-04T09:28:39.267756Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles +2026-04-04T09:28:39.267963Z INFO property_map_server: PMTiles loaded successfully +2026-04-04T09:28:39.302245Z INFO property_map_server: No --dist provided; static serving and OG injection disabled +2026-04-04T09:28:39.327456Z INFO property_map_server: Screenshot service configured: http://screenshot:8002 +2026-04-04T09:28:39.327501Z INFO property_map_server: Precomputed features response groups=7 +2026-04-04T09:28:39.327513Z INFO property_map_server: PocketBase configured: http://pocketbase:8090 +2026-04-04T09:28:39.395026Z INFO property_map_server::pocketbase: PocketBase users collection already has all required fields +2026-04-04T09:28:39.407977Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated +2026-04-04T09:28:39.410968Z INFO property_map_server::pocketbase: PocketBase collection 'saved_properties' API rules updated +2026-04-04T09:28:39.456520Z INFO property_map_server::pocketbase: PocketBase meta.appURL set to https://perfect-postcodes.co.uk/pb +2026-04-04T09:28:39.460437Z INFO property_map_server::pocketbase: PocketBase OAuth configured on users collection +2026-04-04T09:28:39.460481Z INFO property_map_server: Gemini configured (model: gemini-3-flash-preview) +2026-04-04T09:28:39.460494Z INFO property_map_server: Loading travel time data from /app/data/travel-times +2026-04-04T09:28:39.462007Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=2753 +2026-04-04T09:28:39.463115Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=2753 +2026-04-04T09:28:39.464371Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="car" destinations=2753 +2026-04-04T09:28:39.465486Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=2752 +2026-04-04T09:28:39.465504Z INFO property_map_server: Travel time store loaded modes=4 +2026-04-04T09:28:39.465545Z INFO property_map_server: Precomputed AI filters system prompt +2026-04-04T09:28:40.970827Z INFO property_map_server: All memory pages locked (mlockall) +2026-04-04T09:28:40.970864Z INFO property_map_server: Server listening on 0.0.0.0:8001 +2026-04-04T09:28:42.930383Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:28:42.931487Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:28:46.848314Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:28:46.848317Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:28:47.894333Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=7 rows=4416643 parallel=true cells_before_filter=1382 cells_after_filter=1382 truncated=false bounds=51.1305,-0.9325,51.9441,1.3087 filters=2 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:1.6" fields=0 travel_entries=0 grid_ms=0.1 agg_ms=34.8 json_ms=0.7 total_ms=35.6 +2026-04-04T09:28:47.926819Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=4416643 filters=2 total=3082979 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:1.6" ms=65.9 +2026-04-04T09:29:12.967785Z INFO property_map_server: Prometheus metrics initialized +2026-04-04T09:29:12.967973Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet +2026-04-04T09:29:12.967985Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet" +2026-04-04T09:29:13.024153Z INFO property_map_server::data::property: Postcode features loaded rows=1262364 +2026-04-04T09:29:13.024163Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet" +2026-04-04T09:29:15.263127Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203393 +2026-04-04T09:29:15.263137Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet" +2026-04-04T09:29:15.633029Z INFO property_map_server::data::property: buy listings joined rows=1060501 +2026-04-04T09:29:15.633050Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet" +2026-04-04T09:29:15.737844Z INFO property_map_server::data::property: rent listings joined rows=266585 +2026-04-04T09:29:15.737854Z INFO property_map_server::data::property: Concatenating all data sources +2026-04-04T09:29:18.183240Z INFO property_map_server::data::property: All data sources combined properties=15203393 buy_listings=1060501 rent_listings=266585 total=16530479 +2026-04-04T09:29:18.183322Z INFO property_map_server::data::property: Feature columns from config numeric=58 enums=7 total=65 +2026-04-04T09:29:19.513390Z INFO property_map_server::data::property: Combined data selected rows=16530479 +2026-04-04T09:29:19.677045Z INFO property_map_server::data::property: Extracting numeric feature columns +2026-04-04T09:29:20.071686Z INFO property_map_server::data::property: Computing histograms for numeric features +2026-04-04T09:29:21.318336Z INFO property_map_server::data::property: Extracting string columns +2026-04-04T09:29:23.651099Z INFO property_map_server::data::property: Building enum features +2026-04-04T09:29:24.994656Z INFO property_map_server::data::property: Extracting renovation history +2026-04-04T09:29:27.068157Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829784 +2026-04-04T09:29:27.068165Z INFO property_map_server::data::property: Extracting listing features +2026-04-04T09:29:27.751724Z INFO property_map_server::data::property: Listing features extracted properties_with_features=527541 +2026-04-04T09:29:27.751733Z INFO property_map_server::data::property: Sorting rows by spatial locality +2026-04-04T09:29:28.694827Z INFO property_map_server::data::property: Building interned strings +2026-04-04T09:29:34.992916Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted, quantized to u16) +2026-04-04T09:29:37.667623Z INFO property_map_server::data::property: Data loading complete +2026-04-04T09:29:39.273916Z INFO property_map_server: Property data loaded rows=16530479 features=65 enums=7 +2026-04-04T09:29:39.273925Z INFO property_map_server: Building spatial grid index (0.01° cells) +2026-04-04T09:29:39.372482Z INFO property_map_server: Precomputing H3 cells at resolution 12 +2026-04-04T09:29:39.372490Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12 +2026-04-04T09:29:39.769884Z INFO property_map_server::data::property: H3 precomputation complete (16530479 cells) +2026-04-04T09:29:39.769913Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet +2026-04-04T09:29:39.769921Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"... +2026-04-04T09:29:39.787406Z INFO property_map_server::data::poi: Loaded 550611 POIs +2026-04-04T09:29:39.878047Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71 +2026-04-04T09:29:39.878478Z INFO property_map_server::data::poi: POI data loading complete. +2026-04-04T09:29:39.908354Z INFO property_map_server: POI data loaded pois=550611 +2026-04-04T09:29:39.908363Z INFO property_map_server: Building POI spatial grid index +2026-04-04T09:29:39.913096Z INFO property_map_server: Loading place data from /app/data/places.parquet +2026-04-04T09:29:39.913110Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"... +2026-04-04T09:29:39.913711Z INFO property_map_server::data::places: Loaded 3474 places +2026-04-04T09:29:39.914446Z INFO property_map_server::data::places: Place data loaded places=3474 types=2 with_population=71 with_city=3392 +2026-04-04T09:29:39.914500Z INFO property_map_server: Place data loaded places=3474 +2026-04-04T09:29:39.914508Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries +2026-04-04T09:29:39.914511Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries" +2026-04-04T09:29:39.919966Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361 +2026-04-04T09:29:46.391484Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140 +2026-04-04T09:29:46.624748Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140 +2026-04-04T09:29:46.767446Z INFO property_map_server::data::postcodes: Outcode data derived from postcodes outcodes=2361 +2026-04-04T09:29:46.767498Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles +2026-04-04T09:29:46.767759Z INFO property_map_server: PMTiles loaded successfully +2026-04-04T09:29:46.802002Z INFO property_map_server: No --dist provided; static serving and OG injection disabled +2026-04-04T09:29:46.830381Z INFO property_map_server: Screenshot service configured: http://screenshot:8002 +2026-04-04T09:29:46.830436Z INFO property_map_server: Precomputed features response groups=7 +2026-04-04T09:29:46.830456Z INFO property_map_server: PocketBase configured: http://pocketbase:8090 +2026-04-04T09:29:46.876331Z INFO property_map_server::pocketbase: PocketBase users collection already has all required fields +2026-04-04T09:29:46.878597Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated +2026-04-04T09:29:46.881305Z INFO property_map_server::pocketbase: PocketBase collection 'saved_properties' API rules updated +2026-04-04T09:29:46.926624Z INFO property_map_server::pocketbase: PocketBase meta.appURL set to https://perfect-postcodes.co.uk/pb +2026-04-04T09:29:46.929859Z INFO property_map_server::pocketbase: PocketBase OAuth configured on users collection +2026-04-04T09:29:46.929879Z INFO property_map_server: Gemini configured (model: gemini-3-flash-preview) +2026-04-04T09:29:46.929891Z INFO property_map_server: Loading travel time data from /app/data/travel-times +2026-04-04T09:29:46.931340Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=2753 +2026-04-04T09:29:46.932597Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=2753 +2026-04-04T09:29:46.934014Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="car" destinations=2753 +2026-04-04T09:29:46.935056Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=2752 +2026-04-04T09:29:46.935073Z INFO property_map_server: Travel time store loaded modes=4 +2026-04-04T09:29:46.935117Z INFO property_map_server: Precomputed AI filters system prompt +2026-04-04T09:29:48.435168Z INFO property_map_server: All memory pages locked (mlockall) +2026-04-04T09:29:48.435208Z INFO property_map_server: Server listening on 0.0.0.0:8001 +2026-04-04T09:29:53.178063Z INFO property_map_server: Prometheus metrics initialized +2026-04-04T09:29:53.178219Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet +2026-04-04T09:29:53.178227Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet" +2026-04-04T09:29:53.237453Z INFO property_map_server::data::property: Postcode features loaded rows=1262364 +2026-04-04T09:29:53.237463Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet" +2026-04-04T09:29:55.568450Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203393 +2026-04-04T09:29:55.568461Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet" +2026-04-04T09:29:55.952152Z INFO property_map_server::data::property: buy listings joined rows=1060501 +2026-04-04T09:29:55.952176Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet" +2026-04-04T09:29:56.057647Z INFO property_map_server::data::property: rent listings joined rows=266585 +2026-04-04T09:29:56.057657Z INFO property_map_server::data::property: Concatenating all data sources +2026-04-04T09:29:59.507785Z INFO property_map_server::data::property: All data sources combined properties=15203393 buy_listings=1060501 rent_listings=266585 total=16530479 +2026-04-04T09:29:59.507871Z INFO property_map_server::data::property: Feature columns from config numeric=58 enums=7 total=65 +2026-04-04T09:30:00.796614Z INFO property_map_server::data::property: Combined data selected rows=16530479 +2026-04-04T09:30:00.951162Z INFO property_map_server::data::property: Extracting numeric feature columns +2026-04-04T09:30:01.374651Z INFO property_map_server::data::property: Computing histograms for numeric features +2026-04-04T09:30:02.701166Z INFO property_map_server::data::property: Extracting string columns +2026-04-04T09:30:05.005794Z INFO property_map_server::data::property: Building enum features +2026-04-04T09:30:06.640861Z INFO property_map_server::data::property: Extracting renovation history +2026-04-04T09:30:08.691580Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829784 +2026-04-04T09:30:08.691588Z INFO property_map_server::data::property: Extracting listing features +2026-04-04T09:30:09.363379Z INFO property_map_server::data::property: Listing features extracted properties_with_features=527541 +2026-04-04T09:30:09.363389Z INFO property_map_server::data::property: Sorting rows by spatial locality +2026-04-04T09:30:10.294593Z INFO property_map_server::data::property: Building interned strings +2026-04-04T09:30:17.251354Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted, quantized to u16) +2026-04-04T09:30:21.213691Z INFO property_map_server::data::property: Data loading complete +2026-04-04T09:30:23.142430Z INFO property_map_server: Property data loaded rows=16530479 features=65 enums=7 +2026-04-04T09:30:23.142439Z INFO property_map_server: Building spatial grid index (0.01° cells) +2026-04-04T09:30:23.254324Z INFO property_map_server: Precomputing H3 cells at resolution 12 +2026-04-04T09:30:23.254333Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12 +2026-04-04T09:30:23.671848Z INFO property_map_server::data::property: H3 precomputation complete (16530479 cells) +2026-04-04T09:30:23.671900Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet +2026-04-04T09:30:23.671918Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"... +2026-04-04T09:30:23.691917Z INFO property_map_server::data::poi: Loaded 550611 POIs +2026-04-04T09:30:23.787945Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71 +2026-04-04T09:30:23.788397Z INFO property_map_server::data::poi: POI data loading complete. +2026-04-04T09:30:23.817204Z INFO property_map_server: POI data loaded pois=550611 +2026-04-04T09:30:23.817212Z INFO property_map_server: Building POI spatial grid index +2026-04-04T09:30:23.821789Z INFO property_map_server: Loading place data from /app/data/places.parquet +2026-04-04T09:30:23.821797Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"... +2026-04-04T09:30:23.822354Z INFO property_map_server::data::places: Loaded 3474 places +2026-04-04T09:30:23.823118Z INFO property_map_server::data::places: Place data loaded places=3474 types=2 with_population=71 with_city=3392 +2026-04-04T09:30:23.823171Z INFO property_map_server: Place data loaded places=3474 +2026-04-04T09:30:23.823180Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries +2026-04-04T09:30:23.823184Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries" +2026-04-04T09:30:23.825054Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361 +2026-04-04T09:30:30.250379Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140 +2026-04-04T09:30:30.485512Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140 +2026-04-04T09:30:30.626415Z INFO property_map_server::data::postcodes: Outcode data derived from postcodes outcodes=2361 +2026-04-04T09:30:30.626502Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles +2026-04-04T09:30:30.626707Z INFO property_map_server: PMTiles loaded successfully +2026-04-04T09:30:30.662016Z INFO property_map_server: No --dist provided; static serving and OG injection disabled +2026-04-04T09:30:30.704107Z INFO property_map_server: Screenshot service configured: http://screenshot:8002 +2026-04-04T09:30:30.704160Z INFO property_map_server: Precomputed features response groups=7 +2026-04-04T09:30:30.704174Z INFO property_map_server: PocketBase configured: http://pocketbase:8090 +2026-04-04T09:30:30.766670Z INFO property_map_server::pocketbase: PocketBase users collection already has all required fields +2026-04-04T09:30:30.769089Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated +2026-04-04T09:30:30.772132Z INFO property_map_server::pocketbase: PocketBase collection 'saved_properties' API rules updated +2026-04-04T09:30:30.818511Z INFO property_map_server::pocketbase: PocketBase meta.appURL set to https://perfect-postcodes.co.uk/pb +2026-04-04T09:30:30.826566Z INFO property_map_server::pocketbase: PocketBase OAuth configured on users collection +2026-04-04T09:30:30.826591Z INFO property_map_server: Gemini configured (model: gemini-3-flash-preview) +2026-04-04T09:30:30.826606Z INFO property_map_server: Loading travel time data from /app/data/travel-times +2026-04-04T09:30:30.850264Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=2753 +2026-04-04T09:30:30.864447Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=2753 +2026-04-04T09:30:30.873440Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="car" destinations=2753 +2026-04-04T09:30:30.880262Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=2752 +2026-04-04T09:30:30.880285Z INFO property_map_server: Travel time store loaded modes=4 +2026-04-04T09:30:30.880334Z INFO property_map_server: Precomputed AI filters system prompt +2026-04-04T09:30:32.406474Z INFO property_map_server: All memory pages locked (mlockall) +2026-04-04T09:30:32.406509Z INFO property_map_server: Server listening on 0.0.0.0:8001 +2026-04-04T09:30:39.902449Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:30:39.903446Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:31:03.872654Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:31:03.873820Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:32:33.281396Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=7 rows=4416643 parallel=true cells_before_filter=1382 cells_after_filter=1382 truncated=false bounds=51.1305,-0.9325,51.9441,1.3087 filters=2 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:1.6" fields=0 travel_entries=0 grid_ms=0.1 agg_ms=28.9 json_ms=0.6 total_ms=29.5 +2026-04-04T09:32:33.525363Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=4416643 filters=2 travel=0 total=3082979 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:1.6" ms=46.2 +2026-04-04T09:32:35.530435Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:32:35.530456Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:32:35.829427Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=7 rows=4416643 parallel=true cells_before_filter=1382 cells_after_filter=1382 truncated=false bounds=51.1305,-0.9325,51.9441,1.3087 filters=2 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:1.6" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=16.1 json_ms=0.6 total_ms=16.8 +2026-04-04T09:32:36.116576Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=4416643 filters=2 travel=0 total=3082979 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:1.6" ms=43.4 +2026-04-04T09:32:37.768386Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=1600508 parallel=true cells_before_filter=7367 cells_after_filter=7134 truncated=false bounds=51.3931,-0.3637,51.5817,0.1553 filters=2 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:1.6" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=13.0 json_ms=3.4 total_ms=16.5 +2026-04-04T09:32:37.834267Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=1600508 filters=2 travel=0 total=1411422 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:1.6" ms=16.1 +2026-04-04T09:32:38.412094Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:32:38.412113Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:32:38.683682Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=1600508 parallel=true cells_before_filter=7367 cells_after_filter=7182 truncated=false bounds=51.3924,-0.3656,51.5824,0.1572 filters=2 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:1.6" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=12.5 json_ms=3.4 total_ms=15.9 +2026-04-04T09:32:38.923058Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=1600508 filters=2 travel=0 total=1411422 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:1.6" ms=15.5 +2026-04-04T09:32:41.453584Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=1600508 parallel=true cells_before_filter=7531 cells_after_filter=7336 truncated=false bounds=51.3924,-0.3656,51.5824,0.1572 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=12.3 json_ms=5.7 total_ms=18.0 +2026-04-04T09:32:41.609812Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=1600508 parallel=true cells_before_filter=7367 cells_after_filter=7182 truncated=false bounds=51.3924,-0.3656,51.5824,0.1572 filters=2 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:1.6" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=12.9 json_ms=5.9 total_ms=18.8 +2026-04-04T09:32:41.802455Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=1600508 parallel=true cells_before_filter=7117 cells_after_filter=6949 truncated=false bounds=51.3924,-0.3656,51.5824,0.1572 filters=2 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:1.3" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=12.9 json_ms=3.2 total_ms=16.2 +2026-04-04T09:32:42.055074Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=1600508 filters=2 travel=0 total=1370212 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:1.3" ms=15.4 +2026-04-04T09:32:43.065473Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=1600508 parallel=true cells_before_filter=7531 cells_after_filter=7336 truncated=false bounds=51.3924,-0.3656,51.5824,0.1572 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=13.2 json_ms=5.8 total_ms=19.0 +2026-04-04T09:32:43.220274Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=1600508 parallel=true cells_before_filter=7117 cells_after_filter=6949 truncated=false bounds=51.3924,-0.3656,51.5824,0.1572 filters=2 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:1.3" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=13.3 json_ms=5.5 total_ms=18.8 +2026-04-04T09:32:44.046613Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=1600508 parallel=true cells_before_filter=7529 cells_after_filter=7334 truncated=false bounds=51.3924,-0.3656,51.5824,0.1572 filters=2 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:3.1" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=12.7 json_ms=3.5 total_ms=16.2 +2026-04-04T09:32:44.288162Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=1600508 filters=2 travel=0 total=1427822 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:3.1" ms=16.5 +2026-04-04T09:32:45.000361Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=1600508 parallel=true cells_before_filter=7531 cells_after_filter=7336 truncated=false bounds=51.3924,-0.3656,51.5824,0.1572 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=12.2 json_ms=5.6 total_ms=17.9 +2026-04-04T09:32:45.172950Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=1600508 parallel=true cells_before_filter=7529 cells_after_filter=7334 truncated=false bounds=51.3924,-0.3656,51.5824,0.1572 filters=2 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:3.1" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=12.1 json_ms=5.9 total_ms=18.0 +2026-04-04T09:32:45.354310Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=1600508 parallel=true cells_before_filter=7531 cells_after_filter=7336 truncated=false bounds=51.3924,-0.3656,51.5824,0.1572 filters=2 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:5.300000000000001" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=12.7 json_ms=3.8 total_ms=16.6 +2026-04-04T09:32:45.602818Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=1600508 filters=2 travel=0 total=1428012 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:5.300000000000001" ms=15.1 +2026-04-04T09:32:46.915239Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=2836325 parallel=true cells_before_filter=4274 cells_after_filter=4269 truncated=false bounds=51.3025,-0.5450,51.6962,0.5386 filters=2 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:5.300000000000001" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=14.9 json_ms=1.9 total_ms=16.9 +2026-04-04T09:32:47.173216Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=2836325 filters=2 travel=0 total=2563348 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:5.300000000000001" ms=26.4 +2026-04-04T09:32:48.521063Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=2698717 parallel=true cells_before_filter=3754 cells_after_filter=3754 truncated=false bounds=51.3156,-0.5190,51.6798,0.4834 filters=2 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:5.300000000000001" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=13.7 json_ms=1.7 total_ms=15.4 +2026-04-04T09:32:48.771019Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=2698717 filters=2 travel=0 total=2441022 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:5.300000000000001" ms=26.0 +2026-04-04T09:32:49.506790Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3272625 parallel=true cells_before_filter=6094 cells_after_filter=6094 truncated=false bounds=51.2429,-0.6307,51.7359,0.7257 filters=2 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:5.300000000000001" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=16.6 json_ms=2.8 total_ms=19.4 +2026-04-04T09:32:49.830231Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3272625 filters=2 travel=0 total=2948993 filters_raw="Listing status:Historical sale;;Distance to nearest train or tube station (km):0.0017206129:5.300000000000001" ms=31.3 +2026-04-04T09:32:51.648609Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3272625 parallel=true cells_before_filter=6441 cells_after_filter=6441 truncated=false bounds=51.2429,-0.6307,51.7359,0.7257 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=15.5 json_ms=2.8 total_ms=18.4 +2026-04-04T09:32:51.986417Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3272625 filters=1 travel=0 total=2972588 filters_raw="Listing status:Historical sale" ms=93.5 +2026-04-04T09:33:28.076047Z INFO property_map_server: Prometheus metrics initialized +2026-04-04T09:33:28.076197Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet +2026-04-04T09:33:28.076205Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet" +2026-04-04T09:33:28.167559Z INFO property_map_server::data::property: Postcode features loaded rows=1262364 +2026-04-04T09:33:28.167570Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet" +2026-04-04T09:33:30.508853Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203393 +2026-04-04T09:33:30.508864Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet" +2026-04-04T09:33:30.913461Z INFO property_map_server::data::property: buy listings joined rows=1060501 +2026-04-04T09:33:30.913480Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet" +2026-04-04T09:33:31.030377Z INFO property_map_server::data::property: rent listings joined rows=266585 +2026-04-04T09:33:31.030388Z INFO property_map_server::data::property: Concatenating all data sources +2026-04-04T09:33:36.649002Z INFO property_map_server::data::property: All data sources combined properties=15203393 buy_listings=1060501 rent_listings=266585 total=16530479 +2026-04-04T09:33:36.649097Z INFO property_map_server::data::property: Feature columns from config numeric=58 enums=7 total=65 +2026-04-04T09:33:37.802316Z INFO property_map_server::data::property: Combined data selected rows=16530479 +2026-04-04T09:33:37.968239Z INFO property_map_server::data::property: Extracting numeric feature columns +2026-04-04T09:33:38.345282Z INFO property_map_server::data::property: Computing histograms for numeric features +2026-04-04T09:33:39.487522Z INFO property_map_server::data::property: Extracting string columns +2026-04-04T09:33:42.019742Z INFO property_map_server::data::property: Building enum features +2026-04-04T09:33:43.372553Z INFO property_map_server::data::property: Extracting renovation history +2026-04-04T09:33:45.406932Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829784 +2026-04-04T09:33:45.406943Z INFO property_map_server::data::property: Extracting listing features +2026-04-04T09:33:46.080742Z INFO property_map_server::data::property: Listing features extracted properties_with_features=527541 +2026-04-04T09:33:46.080750Z INFO property_map_server::data::property: Sorting rows by spatial locality +2026-04-04T09:33:46.965875Z INFO property_map_server::data::property: Building interned strings +2026-04-04T09:33:53.280241Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted, quantized to u16) +2026-04-04T09:33:55.743136Z INFO property_map_server::data::property: Data loading complete +2026-04-04T09:33:56.820762Z INFO property_map_server: Property data loaded rows=16530479 features=65 enums=7 +2026-04-04T09:33:56.820771Z INFO property_map_server: Building spatial grid index (0.01° cells) +2026-04-04T09:33:57.244448Z INFO property_map_server: Precomputing H3 cells at resolution 12 +2026-04-04T09:33:57.244459Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12 +2026-04-04T09:33:57.649679Z INFO property_map_server::data::property: H3 precomputation complete (16530479 cells) +2026-04-04T09:33:57.649721Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet +2026-04-04T09:33:57.649729Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"... +2026-04-04T09:33:57.670413Z INFO property_map_server::data::poi: Loaded 550611 POIs +2026-04-04T09:33:57.758232Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71 +2026-04-04T09:33:57.758681Z INFO property_map_server::data::poi: POI data loading complete. +2026-04-04T09:33:57.791071Z INFO property_map_server: POI data loaded pois=550611 +2026-04-04T09:33:57.791080Z INFO property_map_server: Building POI spatial grid index +2026-04-04T09:33:57.795491Z INFO property_map_server: Loading place data from /app/data/places.parquet +2026-04-04T09:33:57.795496Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"... +2026-04-04T09:33:57.802226Z INFO property_map_server::data::places: Loaded 3474 places +2026-04-04T09:33:57.802939Z INFO property_map_server::data::places: Place data loaded places=3474 types=2 with_population=71 with_city=3392 +2026-04-04T09:33:57.803001Z INFO property_map_server: Place data loaded places=3474 +2026-04-04T09:33:57.803009Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries +2026-04-04T09:33:57.803012Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries" +2026-04-04T09:33:57.812473Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361 +2026-04-04T09:34:37.054421Z INFO property_map_server: Prometheus metrics initialized +2026-04-04T09:34:37.054637Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet +2026-04-04T09:34:37.054657Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet" +2026-04-04T09:34:37.112188Z INFO property_map_server::data::property: Postcode features loaded rows=1262364 +2026-04-04T09:34:37.112198Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet" +2026-04-04T09:34:45.751330Z INFO property_map_server: Prometheus metrics initialized +2026-04-04T09:34:45.751484Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet +2026-04-04T09:34:45.751491Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet" +2026-04-04T09:34:45.807033Z INFO property_map_server::data::property: Postcode features loaded rows=1262364 +2026-04-04T09:34:45.807042Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet" +2026-04-04T09:34:47.919170Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203393 +2026-04-04T09:34:47.919180Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet" +2026-04-04T09:34:48.270494Z INFO property_map_server::data::property: buy listings joined rows=1060501 +2026-04-04T09:34:48.270513Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet" +2026-04-04T09:34:48.374961Z INFO property_map_server::data::property: rent listings joined rows=266585 +2026-04-04T09:34:48.374970Z INFO property_map_server::data::property: Concatenating all data sources +2026-04-04T09:34:52.368930Z INFO property_map_server::data::property: All data sources combined properties=15203393 buy_listings=1060501 rent_listings=266585 total=16530479 +2026-04-04T09:34:52.368999Z INFO property_map_server::data::property: Feature columns from config numeric=58 enums=7 total=65 +2026-04-04T09:34:53.643464Z INFO property_map_server::data::property: Combined data selected rows=16530479 +2026-04-04T09:34:53.804068Z INFO property_map_server::data::property: Extracting numeric feature columns +2026-04-04T09:34:54.211869Z INFO property_map_server::data::property: Computing histograms for numeric features +2026-04-04T09:34:55.489765Z INFO property_map_server::data::property: Extracting string columns +2026-04-04T09:34:57.733502Z INFO property_map_server::data::property: Building enum features +2026-04-04T09:35:05.727722Z INFO property_map_server: Prometheus metrics initialized +2026-04-04T09:35:05.727913Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet +2026-04-04T09:35:05.727926Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet" +2026-04-04T09:35:05.786709Z INFO property_map_server::data::property: Postcode features loaded rows=1262364 +2026-04-04T09:35:05.786718Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet" +2026-04-04T09:35:08.025257Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203393 +2026-04-04T09:35:08.025268Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet" +2026-04-04T09:35:08.396676Z INFO property_map_server::data::property: buy listings joined rows=1060501 +2026-04-04T09:35:08.396694Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet" +2026-04-04T09:35:08.504367Z INFO property_map_server::data::property: rent listings joined rows=266585 +2026-04-04T09:35:08.504377Z INFO property_map_server::data::property: Concatenating all data sources +2026-04-04T09:35:10.708697Z INFO property_map_server::data::property: All data sources combined properties=15203393 buy_listings=1060501 rent_listings=266585 total=16530479 +2026-04-04T09:35:10.708778Z INFO property_map_server::data::property: Feature columns from config numeric=58 enums=7 total=65 +2026-04-04T09:35:11.928376Z INFO property_map_server::data::property: Combined data selected rows=16530479 +2026-04-04T09:35:12.112507Z INFO property_map_server::data::property: Extracting numeric feature columns +2026-04-04T09:35:12.539597Z INFO property_map_server::data::property: Computing histograms for numeric features +2026-04-04T09:35:13.717388Z INFO property_map_server::data::property: Extracting string columns +2026-04-04T09:35:16.038975Z INFO property_map_server::data::property: Building enum features +2026-04-04T09:35:17.393465Z INFO property_map_server::data::property: Extracting renovation history +2026-04-04T09:35:23.650832Z INFO property_map_server: Prometheus metrics initialized +2026-04-04T09:35:23.650999Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet +2026-04-04T09:35:23.651008Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet" +2026-04-04T09:35:23.708191Z INFO property_map_server::data::property: Postcode features loaded rows=1262364 +2026-04-04T09:35:23.708203Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet" +2026-04-04T09:35:25.961705Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203393 +2026-04-04T09:35:25.961715Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet" +2026-04-04T09:35:26.315187Z INFO property_map_server::data::property: buy listings joined rows=1060501 +2026-04-04T09:35:26.315213Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet" +2026-04-04T09:35:26.429728Z INFO property_map_server::data::property: rent listings joined rows=266585 +2026-04-04T09:35:26.429739Z INFO property_map_server::data::property: Concatenating all data sources +2026-04-04T09:35:28.639127Z INFO property_map_server::data::property: All data sources combined properties=15203393 buy_listings=1060501 rent_listings=266585 total=16530479 +2026-04-04T09:35:28.639203Z INFO property_map_server::data::property: Feature columns from config numeric=58 enums=7 total=65 +2026-04-04T09:35:29.877107Z INFO property_map_server::data::property: Combined data selected rows=16530479 +2026-04-04T09:35:30.054805Z INFO property_map_server::data::property: Extracting numeric feature columns +2026-04-04T09:35:30.483812Z INFO property_map_server::data::property: Computing histograms for numeric features +2026-04-04T09:35:31.662133Z INFO property_map_server::data::property: Extracting string columns +2026-04-04T09:35:33.886579Z INFO property_map_server::data::property: Building enum features +2026-04-04T09:35:35.250064Z INFO property_map_server::data::property: Extracting renovation history +2026-04-04T09:35:39.879901Z INFO property_map_server: Prometheus metrics initialized +2026-04-04T09:35:39.880057Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet +2026-04-04T09:35:39.880065Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet" +2026-04-04T09:35:39.938249Z INFO property_map_server::data::property: Postcode features loaded rows=1262364 +2026-04-04T09:35:39.938259Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet" +2026-04-04T09:35:42.126026Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203393 +2026-04-04T09:35:42.126037Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet" +2026-04-04T09:35:42.501607Z INFO property_map_server::data::property: buy listings joined rows=1060501 +2026-04-04T09:35:42.501626Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet" +2026-04-04T09:35:42.602902Z INFO property_map_server::data::property: rent listings joined rows=266585 +2026-04-04T09:35:42.602914Z INFO property_map_server::data::property: Concatenating all data sources +2026-04-04T09:35:44.954760Z INFO property_map_server::data::property: All data sources combined properties=15203393 buy_listings=1060501 rent_listings=266585 total=16530479 +2026-04-04T09:35:44.954863Z INFO property_map_server::data::property: Feature columns from config numeric=58 enums=7 total=65 +2026-04-04T09:35:46.128858Z INFO property_map_server::data::property: Combined data selected rows=16530479 +2026-04-04T09:35:46.312380Z INFO property_map_server::data::property: Extracting numeric feature columns +2026-04-04T09:35:46.754919Z INFO property_map_server::data::property: Computing histograms for numeric features +2026-04-04T09:35:47.921519Z INFO property_map_server::data::property: Extracting string columns +2026-04-04T09:35:50.259285Z INFO property_map_server::data::property: Building enum features +2026-04-04T09:35:51.631288Z INFO property_map_server::data::property: Extracting renovation history +2026-04-04T09:35:53.754130Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829784 +2026-04-04T09:35:53.754139Z INFO property_map_server::data::property: Extracting listing features +2026-04-04T09:35:54.412095Z INFO property_map_server::data::property: Listing features extracted properties_with_features=527541 +2026-04-04T09:35:54.412104Z INFO property_map_server::data::property: Sorting rows by spatial locality +2026-04-04T09:35:55.248750Z INFO property_map_server::data::property: Building interned strings +2026-04-04T09:36:01.492024Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted, quantized to u16) +2026-04-04T09:36:04.022839Z INFO property_map_server::data::property: Data loading complete +2026-04-04T09:36:05.081113Z INFO property_map_server: Property data loaded rows=16530479 features=65 enums=7 +2026-04-04T09:36:05.081122Z INFO property_map_server: Building spatial grid index (0.01° cells) +2026-04-04T09:36:05.502537Z INFO property_map_server: Precomputing H3 cells at resolution 12 +2026-04-04T09:36:05.502553Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12 +2026-04-04T09:36:05.908122Z INFO property_map_server::data::property: H3 precomputation complete (16530479 cells) +2026-04-04T09:36:05.908147Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet +2026-04-04T09:36:05.908152Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"... +2026-04-04T09:36:05.935944Z INFO property_map_server::data::poi: Loaded 550611 POIs +2026-04-04T09:36:06.024473Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71 +2026-04-04T09:36:06.025054Z INFO property_map_server::data::poi: POI data loading complete. +2026-04-04T09:36:06.053946Z INFO property_map_server: POI data loaded pois=550611 +2026-04-04T09:36:06.053956Z INFO property_map_server: Building POI spatial grid index +2026-04-04T09:36:06.058516Z INFO property_map_server: Loading place data from /app/data/places.parquet +2026-04-04T09:36:06.058523Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"... +2026-04-04T09:36:06.060428Z INFO property_map_server::data::places: Loaded 3474 places +2026-04-04T09:36:06.061164Z INFO property_map_server::data::places: Place data loaded places=3474 types=2 with_population=71 with_city=3392 +2026-04-04T09:36:06.061214Z INFO property_map_server: Place data loaded places=3474 +2026-04-04T09:36:06.061222Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries +2026-04-04T09:36:06.061226Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries" +2026-04-04T09:36:06.061982Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361 +2026-04-04T09:36:12.939557Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140 +2026-04-04T09:36:13.164618Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140 +2026-04-04T09:36:13.299287Z INFO property_map_server::data::postcodes: Outcode data derived from postcodes outcodes=2361 +2026-04-04T09:36:13.299351Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles +2026-04-04T09:36:13.299563Z INFO property_map_server: PMTiles loaded successfully +2026-04-04T09:36:13.333753Z INFO property_map_server: No --dist provided; static serving and OG injection disabled +2026-04-04T09:36:13.385493Z INFO property_map_server: Screenshot service configured: http://screenshot:8002 +2026-04-04T09:36:13.385543Z INFO property_map_server: Precomputed features response groups=7 +2026-04-04T09:36:13.385555Z INFO property_map_server: PocketBase configured: http://pocketbase:8090 +2026-04-04T09:36:13.459165Z INFO property_map_server::pocketbase: PocketBase users collection already has all required fields +2026-04-04T09:36:13.467107Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated +2026-04-04T09:36:13.470789Z INFO property_map_server::pocketbase: PocketBase collection 'saved_properties' API rules updated +2026-04-04T09:36:13.517400Z INFO property_map_server::pocketbase: PocketBase meta.appURL set to https://perfect-postcodes.co.uk/pb +2026-04-04T09:36:13.521396Z INFO property_map_server::pocketbase: PocketBase OAuth configured on users collection +2026-04-04T09:36:13.521436Z INFO property_map_server: Gemini configured (model: gemini-3-flash-preview) +2026-04-04T09:36:13.521453Z INFO property_map_server: Loading travel time data from /app/data/travel-times +2026-04-04T09:36:13.523210Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=2753 +2026-04-04T09:36:13.524868Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=2753 +2026-04-04T09:36:13.526325Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="car" destinations=2753 +2026-04-04T09:36:13.527464Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=2752 +2026-04-04T09:36:13.527485Z INFO property_map_server: Travel time store loaded modes=4 +2026-04-04T09:36:13.527526Z INFO property_map_server: Precomputed AI filters system prompt +2026-04-04T09:36:26.321388Z INFO property_map_server: All memory pages locked (mlockall) +2026-04-04T09:36:26.321433Z INFO property_map_server: Server listening on 0.0.0.0:8001 +2026-04-04T09:36:26.891326Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:36:26.892565Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:36:51.111962Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:36:51.114204Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:36:51.362159Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:36:51.363308Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:36:51.765674Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3349136 parallel=true cells_before_filter=6777 cells_after_filter=6762 truncated=false bounds=51.2387,-0.6423,51.7401,0.7373 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=23.9 json_ms=3.1 total_ms=27.0 +2026-04-04T09:36:52.075284Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3349136 filters=1 travel=0 total=3042652 filters_raw="Listing status:Historical sale" ms=98.7 +2026-04-04T09:38:42.995572Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=88194adb4dfffff resolution=8 total_count=969 filters=1 filters_raw="Listing status:Historical sale" ms=0.6 +2026-04-04T09:38:43.184010Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=5762 cells_after_filter=5751 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=18.3 json_ms=2.7 total_ms=21.1 +2026-04-04T09:38:43.530194Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3104843 filters=1 travel=0 total=2818666 filters_raw="Listing status:Historical sale" ms=91.3 +2026-04-04T09:38:47.289883Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=88194adb4dfffff resolution=8 total_count=969 filters=2 filters_raw="Listing status:Historical sale;;Last known price:50000:inf" ms=0.5 +2026-04-04T09:38:47.451123Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=5762 cells_after_filter=5751 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=2 filters_raw="Listing status:Historical sale;;Last known price:50000:inf" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=17.1 json_ms=2.6 total_ms=19.7 +2026-04-04T09:38:47.711731Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3104843 filters=2 travel=0 total=2818666 filters_raw="Listing status:Historical sale;;Last known price:50000:inf" ms=31.0 +2026-04-04T09:38:49.111013Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=5762 cells_after_filter=5751 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=16.7 json_ms=4.1 total_ms=20.8 +2026-04-04T09:38:49.280727Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=5762 cells_after_filter=5751 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=2 filters_raw="Listing status:Historical sale;;Last known price:50000:inf" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=16.1 json_ms=3.7 total_ms=19.9 +2026-04-04T09:38:49.739926Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=88194adb4dfffff resolution=8 total_count=24 filters=2 filters_raw="Listing status:Historical sale;;Last known price:1070000:inf" ms=0.2 +2026-04-04T09:38:49.913762Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=3429 cells_after_filter=3427 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=2 filters_raw="Listing status:Historical sale;;Last known price:1070000:inf" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=9.8 json_ms=1.5 total_ms=11.4 +2026-04-04T09:38:50.176023Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3104843 filters=2 travel=0 total=161439 filters_raw="Listing status:Historical sale;;Last known price:1070000:inf" ms=29.8 +2026-04-04T09:38:50.832627Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=5762 cells_after_filter=5751 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=16.7 json_ms=4.9 total_ms=21.7 +2026-04-04T09:38:50.982407Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=3429 cells_after_filter=3427 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=2 filters_raw="Listing status:Historical sale;;Last known price:1070000:inf" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=10.8 json_ms=2.3 total_ms=13.0 +2026-04-04T09:38:51.107231Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=88194adb4dfffff resolution=8 total_count=4 filters=2 filters_raw="Listing status:Historical sale;;Last known price:1390000:inf" ms=0.1 +2026-04-04T09:38:51.241111Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=2532 cells_after_filter=2531 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=2 filters_raw="Listing status:Historical sale;;Last known price:1390000:inf" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=9.6 json_ms=1.1 total_ms=10.8 +2026-04-04T09:38:51.498959Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3104843 filters=2 travel=0 total=94045 filters_raw="Listing status:Historical sale;;Last known price:1390000:inf" ms=29.0 +2026-04-04T09:38:51.775881Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=5762 cells_after_filter=5751 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=15.2 json_ms=4.0 total_ms=19.2 +2026-04-04T09:38:51.906003Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=2532 cells_after_filter=2531 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=2 filters_raw="Listing status:Historical sale;;Last known price:1390000:inf" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=9.4 json_ms=1.7 total_ms=11.2 +2026-04-04T09:38:51.960913Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=88194adb4dfffff resolution=8 total_count=3 filters=2 filters_raw="Listing status:Historical sale;;Last known price:1650000:inf" ms=0.2 +2026-04-04T09:38:52.137355Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=1986 cells_after_filter=1986 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=2 filters_raw="Listing status:Historical sale;;Last known price:1650000:inf" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=9.4 json_ms=0.9 total_ms=10.3 +2026-04-04T09:38:52.382178Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3104843 filters=2 travel=0 total=65641 filters_raw="Listing status:Historical sale;;Last known price:1650000:inf" ms=30.6 +2026-04-04T09:38:53.008909Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=5762 cells_after_filter=5751 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=15.4 json_ms=4.3 total_ms=19.7 +2026-04-04T09:38:53.180967Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=1986 cells_after_filter=1986 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=2 filters_raw="Listing status:Historical sale;;Last known price:1650000:inf" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=8.3 json_ms=1.2 total_ms=9.6 +2026-04-04T09:38:53.279134Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=88194adb4dfffff resolution=8 total_count=17 filters=2 filters_raw="Listing status:Historical sale;;Last known price:1200000:inf" ms=0.2 +2026-04-04T09:38:53.443663Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=3099 cells_after_filter=3097 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=2 filters_raw="Listing status:Historical sale;;Last known price:1200000:inf" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=10.1 json_ms=1.3 total_ms=11.4 +2026-04-04T09:38:53.708661Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3104843 filters=2 travel=0 total=131277 filters_raw="Listing status:Historical sale;;Last known price:1200000:inf" ms=28.8 +2026-04-04T09:38:53.843286Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=5762 cells_after_filter=5751 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=16.3 json_ms=4.2 total_ms=20.5 +2026-04-04T09:38:53.991436Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=3099 cells_after_filter=3097 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=2 filters_raw="Listing status:Historical sale;;Last known price:1200000:inf" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=10.0 json_ms=2.0 total_ms=12.0 +2026-04-04T09:38:54.052964Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=88194adb4dfffff resolution=8 total_count=63 filters=2 filters_raw="Listing status:Historical sale;;Last known price:920000:inf" ms=0.2 +2026-04-04T09:38:54.236037Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=3939 cells_after_filter=3937 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=2 filters_raw="Listing status:Historical sale;;Last known price:920000:inf" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=11.0 json_ms=1.8 total_ms=12.8 +2026-04-04T09:38:54.483840Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3104843 filters=2 travel=0 total=215645 filters_raw="Listing status:Historical sale;;Last known price:920000:inf" ms=29.6 +2026-04-04T09:38:54.602700Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=5762 cells_after_filter=5751 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=15.0 json_ms=3.9 total_ms=18.9 +2026-04-04T09:38:54.742059Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=3939 cells_after_filter=3937 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=2 filters_raw="Listing status:Historical sale;;Last known price:920000:inf" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=10.3 json_ms=2.5 total_ms=12.9 +2026-04-04T09:38:54.974636Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=88194adb4dfffff resolution=8 total_count=265 filters=2 filters_raw="Listing status:Historical sale;;Last known price:500000:inf" ms=0.2 +2026-04-04T09:38:55.140085Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=5446 cells_after_filter=5439 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=2 filters_raw="Listing status:Historical sale;;Last known price:500000:inf" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=13.6 json_ms=2.4 total_ms=16.0 +2026-04-04T09:38:55.401827Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3104843 filters=2 travel=0 total=747405 filters_raw="Listing status:Historical sale;;Last known price:500000:inf" ms=33.2 +2026-04-04T09:39:12.394962Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=88194adb4dfffff resolution=8 total_count=265 filters=3 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other" ms=0.3 +2026-04-04T09:39:12.565873Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=5446 cells_after_filter=5439 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=3 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=16.9 json_ms=2.4 total_ms=19.4 +2026-04-04T09:39:12.833192Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3104843 filters=3 travel=0 total=747405 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other" ms=47.3 +2026-04-04T09:39:14.713978Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=88194adb4dfffff resolution=8 total_count=235 filters=4 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744" ms=0.2 +2026-04-04T09:39:14.885527Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=5388 cells_after_filter=5381 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=4 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=13.1 json_ms=2.3 total_ms=15.5 +2026-04-04T09:39:15.162903Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3104843 filters=4 travel=0 total=683785 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744" ms=47.3 +2026-04-04T09:39:15.378577Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=88194adb4dfffff resolution=8 total_count=203 filters=5 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750" ms=0.2 +2026-04-04T09:39:15.539135Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=5305 cells_after_filter=5298 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=5 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=13.4 json_ms=2.3 total_ms=15.8 +2026-04-04T09:39:15.840770Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3104843 filters=5 travel=0 total=566384 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750" ms=49.0 +2026-04-04T09:39:16.247382Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=88194adb4dfffff resolution=8 total_count=192 filters=5 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750" ms=0.2 +2026-04-04T09:39:16.414585Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3104843 parallel=true cells_before_filter=4780 cells_after_filter=4776 truncated=false bounds=51.2387,-0.5206,51.7401,0.6156 filters=5 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=12.8 json_ms=2.1 total_ms=14.9 +2026-04-04T09:39:16.721763Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3104843 filters=5 travel=0 total=459500 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750" ms=49.9 +2026-04-04T09:39:38.356994Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:39:38.359570Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:40:01.249033Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4 +2026-04-04T09:40:01.700722Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3104843 filters=5 travel=0 total=459500 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750" ms=51.2 +2026-04-04T09:40:04.415006Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3099349 parallel=true cells_before_filter=3833 cells_after_filter=3832 truncated=false bounds=51.2619,-0.6575,51.7630,0.4787 filters=5 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=15.4 json_ms=2.4 total_ms=17.9 +2026-04-04T09:40:04.758545Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3099349 filters=6 travel=1 total=461535 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750" ms=112.0 +2026-04-04T09:40:05.327970Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3099349 parallel=true cells_before_filter=3833 cells_after_filter=3832 truncated=false bounds=51.2619,-0.6575,51.7630,0.4787 filters=5 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=15.1 json_ms=2.4 total_ms=17.5 +2026-04-04T09:40:05.481789Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3099349 parallel=true cells_before_filter=3833 cells_after_filter=3832 truncated=false bounds=51.2619,-0.6575,51.7630,0.4787 filters=5 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=15.0 json_ms=2.8 total_ms=17.8 +2026-04-04T09:40:05.665545Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3099349 parallel=true cells_before_filter=1675 cells_after_filter=1675 truncated=false bounds=51.2619,-0.6575,51.7630,0.4787 filters=5 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=12.7 json_ms=1.5 total_ms=14.3 +2026-04-04T09:40:06.005010Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3099349 filters=6 travel=1 total=353301 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750" ms=111.8 +2026-04-04T09:40:06.931172Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3099349 parallel=true cells_before_filter=3833 cells_after_filter=3832 truncated=false bounds=51.2619,-0.6575,51.7630,0.4787 filters=5 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=14.5 json_ms=2.3 total_ms=16.8 +2026-04-04T09:40:07.096101Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3099349 parallel=true cells_before_filter=1675 cells_after_filter=1675 truncated=false bounds=51.2619,-0.6575,51.7630,0.4787 filters=5 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=12.3 json_ms=1.0 total_ms=13.3 +2026-04-04T09:40:07.306162Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3099349 parallel=true cells_before_filter=1455 cells_after_filter=1455 truncated=false bounds=51.2619,-0.6575,51.7630,0.4787 filters=5 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=10.9 json_ms=0.9 total_ms=11.8 +2026-04-04T09:40:07.647130Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3099349 filters=6 travel=1 total=199600 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750" ms=110.7 +2026-04-04T09:40:33.342454Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4 +2026-04-04T09:40:45.290970Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=88194adb4dfffff resolution=8 total_count=192 filters=6 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" ms=0.2 +2026-04-04T09:40:45.292103Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.3 +2026-04-04T09:40:45.437129Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3099349 parallel=true cells_before_filter=1455 cells_after_filter=1455 truncated=false bounds=51.2619,-0.6575,51.7630,0.4787 filters=6 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=11.3 json_ms=0.9 total_ms=12.2 +2026-04-04T09:40:45.794892Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3099349 filters=7 travel=1 total=199600 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" ms=113.2 +2026-04-04T09:40:50.160634Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.2 +2026-04-04T09:40:53.059037Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.2 +2026-04-04T09:40:54.127947Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.3 +2026-04-04T09:43:43.775140Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:43:43.776685Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:43:43.837914Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4 +2026-04-04T09:43:43.846627Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:43:43.847665Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:43:43.974313Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.3 +2026-04-04T09:44:04.558780Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:44:04.567512Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:44:04.635515Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.3 +2026-04-04T09:44:04.673426Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:44:04.673428Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:44:04.723503Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4 +2026-04-04T09:44:06.051705Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:44:06.055278Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:44:06.099983Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.3 +2026-04-04T09:44:06.117044Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:44:06.117093Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:44:06.204860Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4 +2026-04-04T09:44:07.831434Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:44:07.831436Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:44:08.048428Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.6 +2026-04-04T09:44:08.052709Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:44:08.055009Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:44:08.112231Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.3 +2026-04-04T09:44:09.247098Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:44:09.247397Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:44:09.381965Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.4 +2026-04-04T09:44:09.385236Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:44:09.385294Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:44:09.471533Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.2 +2026-04-04T09:44:57.873840Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.3 +2026-04-04T09:44:57.885073Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:44:57.886036Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:44:57.932679Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.2 +2026-04-04T09:45:00.694289Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=3403304 parallel=true cells_before_filter=1455 cells_after_filter=1455 truncated=false bounds=51.2619,-0.7792,51.7630,0.6004 filters=6 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=13.4 json_ms=1.3 total_ms=14.7 +2026-04-04T09:45:01.053501Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3403304 filters=7 travel=1 total=199600 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" ms=125.1 +2026-04-04T09:45:21.013582Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3403304 filters=7 travel=1 total=310116 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" ms=119.3 +2026-04-04T09:45:22.289298Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=84 cells_after_filter=61 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=6 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.5 json_ms=0.1 total_ms=1.6 +2026-04-04T09:45:22.602222Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=310268 filters=7 travel=1 total=7652 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" ms=11.9 +2026-04-04T09:45:23.648923Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=825 cells_after_filter=679 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=6 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.6 json_ms=0.5 total_ms=3.1 +2026-04-04T09:45:23.796861Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=84 cells_after_filter=61 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=6 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.5 json_ms=0.1 total_ms=1.6 +2026-04-04T09:45:24.005856Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=84 cells_after_filter=61 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=6 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.6 json_ms=0.1 total_ms=1.6 +2026-04-04T09:45:24.259736Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=310268 filters=7 travel=1 total=7652 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" ms=12.5 +2026-04-04T09:45:25.431775Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=825 cells_after_filter=679 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=6 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.7 json_ms=0.5 total_ms=3.2 +2026-04-04T09:45:25.579030Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=84 cells_after_filter=61 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=6 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=1.3 json_ms=0.1 total_ms=1.4 +2026-04-04T09:45:25.890095Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=604 cells_after_filter=474 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=6 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.2 json_ms=0.3 total_ms=2.5 +2026-04-04T09:45:26.132252Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=310268 filters=7 travel=1 total=71322 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" ms=12.1 +2026-04-04T09:45:26.194904Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=825 cells_after_filter=679 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=6 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.6 json_ms=0.5 total_ms=3.1 +2026-04-04T09:45:26.357902Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=604 cells_after_filter=474 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=6 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=3.3 json_ms=0.3 total_ms=3.6 +2026-04-04T09:45:26.557308Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=796 cells_after_filter=651 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=6 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=1 grid_ms=0.0 agg_ms=2.6 json_ms=0.4 total_ms=3.0 +2026-04-04T09:45:26.798396Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=310268 filters=7 travel=1 total=87928 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" ms=12.0 +2026-04-04T09:45:29.312464Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=825 cells_after_filter=679 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=6 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.1 json_ms=0.4 total_ms=2.5 +2026-04-04T09:45:29.576178Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=310268 filters=6 travel=0 total=90065 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" ms=5.6 +2026-04-04T09:46:14.028136Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=24630 parallel=false cells_before_filter=101 cells_after_filter=57 truncated=false bounds=51.5095,-0.1429,51.5205,-0.1171 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.1 json_ms=0.0 total_ms=1.1 +2026-04-04T09:46:14.277640Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=24630 filters=1 travel=0 total=18876 filters_raw="Listing status:Historical sale" ms=0.8 +2026-04-04T09:46:16.369236Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=17864 parallel=false cells_before_filter=81 cells_after_filter=55 truncated=false bounds=51.5095,-0.1399,51.5204,-0.1140 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.0 json_ms=0.0 total_ms=1.1 +2026-04-04T09:46:17.046556Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=52 cells_after_filter=43 truncated=false bounds=51.5081,-0.1424,51.5191,-0.1165 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.5 json_ms=0.0 total_ms=0.5 +2026-04-04T09:46:17.300914Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=10805 filters=1 travel=0 total=8405 filters_raw="Listing status:Historical sale" ms=0.4 +2026-04-04T09:48:39.871222Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=52 cells_after_filter=43 truncated=false bounds=51.5081,-0.1424,51.5191,-0.1165 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.5 json_ms=0.0 total_ms=0.6 +2026-04-04T09:48:40.125658Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=10805 filters=1 travel=0 total=8405 filters_raw="Listing status:Historical sale" ms=0.4 +2026-04-04T09:48:40.877147Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=825 cells_after_filter=679 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=6 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.7 json_ms=0.4 total_ms=3.1 +2026-04-04T09:48:40.879704Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=310268 filters=6 travel=0 total=90065 filters_raw="Listing status:Historical sale;;Last known price:500000:inf;;Property type:Semi-Detached|Terraced|Flats/Maisonettes|Other;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" ms=5.8 +2026-04-04T09:50:23.090149Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=52 cells_after_filter=43 truncated=false bounds=51.5081,-0.1424,51.5191,-0.1166 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.5 json_ms=0.0 total_ms=0.5 +2026-04-04T09:50:23.344308Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=10805 filters=1 travel=0 total=8405 filters_raw="Listing status:Historical sale" ms=0.4 +2026-04-04T09:50:25.604663Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=17864 parallel=false cells_before_filter=81 cells_after_filter=69 truncated=false bounds=51.5043,-0.1424,51.5229,-0.1166 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.1 json_ms=0.1 total_ms=1.2 +2026-04-04T09:50:26.722951Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=21373 parallel=false cells_before_filter=80 cells_after_filter=62 truncated=false bounds=51.4995,-0.1421,51.5182,-0.1163 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.0 json_ms=0.0 total_ms=1.0 +2026-04-04T09:50:26.973810Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=21373 filters=1 travel=0 total=16983 filters_raw="Listing status:Historical sale" ms=0.7 +2026-04-04T09:50:27.960736Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=52 cells_after_filter=49 truncated=false bounds=51.5021,-0.1421,51.5156,-0.1163 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.5 json_ms=0.0 total_ms=0.5 +2026-04-04T09:50:28.209407Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=10805 filters=1 travel=0 total=8405 filters_raw="Listing status:Historical sale" ms=0.4 +2026-04-04T09:51:22.891022Z INFO property_map_server: Prometheus metrics initialized +2026-04-04T09:51:22.891197Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet +2026-04-04T09:51:22.891207Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet" +2026-04-04T09:51:23.017879Z INFO property_map_server::data::property: Postcode features loaded rows=1262364 +2026-04-04T09:51:23.017889Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet" +2026-04-04T09:51:25.325821Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203393 +2026-04-04T09:51:25.325831Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet" +2026-04-04T09:51:25.713903Z INFO property_map_server::data::property: buy listings joined rows=1060501 +2026-04-04T09:51:25.713923Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet" +2026-04-04T09:51:25.821290Z INFO property_map_server::data::property: rent listings joined rows=266585 +2026-04-04T09:51:25.821302Z INFO property_map_server::data::property: Concatenating all data sources +2026-04-04T09:51:34.949514Z INFO property_map_server::data::property: All data sources combined properties=15203393 buy_listings=1060501 rent_listings=266585 total=16530479 +2026-04-04T09:51:34.949603Z INFO property_map_server::data::property: Feature columns from config numeric=58 enums=7 total=65 +2026-04-04T09:51:36.272343Z INFO property_map_server::data::property: Combined data selected rows=16530479 +2026-04-04T09:51:36.456092Z INFO property_map_server::data::property: Extracting numeric feature columns +2026-04-04T09:51:36.874329Z INFO property_map_server::data::property: Computing histograms for numeric features +2026-04-04T09:51:38.042186Z INFO property_map_server::data::property: Extracting string columns +2026-04-04T09:51:40.439359Z INFO property_map_server::data::property: Building enum features +2026-04-04T09:51:41.800807Z INFO property_map_server::data::property: Extracting renovation history +2026-04-04T09:51:43.933883Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829784 +2026-04-04T09:51:43.933893Z INFO property_map_server::data::property: Extracting listing features +2026-04-04T09:51:44.636267Z INFO property_map_server::data::property: Listing features extracted properties_with_features=527541 +2026-04-04T09:51:44.636279Z INFO property_map_server::data::property: Sorting rows by spatial locality +2026-04-04T09:51:45.698096Z INFO property_map_server::data::property: Building interned strings +2026-04-04T09:51:52.960609Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted, quantized to u16) +2026-04-04T09:51:55.690313Z INFO property_map_server::data::property: Data loading complete +2026-04-04T09:51:56.943314Z INFO property_map_server: Property data loaded rows=16530479 features=65 enums=7 +2026-04-04T09:51:56.943322Z INFO property_map_server: Building spatial grid index (0.01° cells) +2026-04-04T09:51:57.363980Z INFO property_map_server: Precomputing H3 cells at resolution 12 +2026-04-04T09:51:57.363989Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12 +2026-04-04T09:51:57.756811Z INFO property_map_server::data::property: H3 precomputation complete (16530479 cells) +2026-04-04T09:51:57.756843Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet +2026-04-04T09:51:57.756848Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"... +2026-04-04T09:51:57.786577Z INFO property_map_server::data::poi: Loaded 550611 POIs +2026-04-04T09:51:57.874082Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71 +2026-04-04T09:51:57.874542Z INFO property_map_server::data::poi: POI data loading complete. +2026-04-04T09:51:57.903229Z INFO property_map_server: POI data loaded pois=550611 +2026-04-04T09:51:57.903238Z INFO property_map_server: Building POI spatial grid index +2026-04-04T09:51:57.907863Z INFO property_map_server: Loading place data from /app/data/places.parquet +2026-04-04T09:51:57.907870Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"... +2026-04-04T09:51:57.908430Z INFO property_map_server::data::places: Loaded 3474 places +2026-04-04T09:51:57.909199Z INFO property_map_server::data::places: Place data loaded places=3474 types=2 with_population=71 with_city=3392 +2026-04-04T09:51:57.909252Z INFO property_map_server: Place data loaded places=3474 +2026-04-04T09:51:57.909261Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries +2026-04-04T09:51:57.909264Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries" +2026-04-04T09:51:57.910057Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361 +2026-04-04T09:52:06.113462Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140 +2026-04-04T09:52:06.336341Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140 +2026-04-04T09:52:06.472031Z INFO property_map_server::data::postcodes: Outcode data derived from postcodes outcodes=2361 +2026-04-04T09:52:06.472086Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles +2026-04-04T09:52:06.472443Z INFO property_map_server: PMTiles loaded successfully +2026-04-04T09:52:06.506874Z INFO property_map_server: No --dist provided; static serving and OG injection disabled +2026-04-04T09:52:06.536291Z INFO property_map_server: Screenshot service configured: http://screenshot:8002 +2026-04-04T09:52:06.536344Z INFO property_map_server: Precomputed features response groups=7 +2026-04-04T09:52:06.536358Z INFO property_map_server: PocketBase configured: http://pocketbase:8090 +2026-04-04T09:52:06.643629Z INFO property_map_server::pocketbase: PocketBase users collection already has all required fields +2026-04-04T09:52:06.658130Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated +2026-04-04T09:52:06.661422Z INFO property_map_server::pocketbase: PocketBase collection 'saved_properties' API rules updated +2026-04-04T09:52:06.707837Z INFO property_map_server::pocketbase: PocketBase meta.appURL set to https://perfect-postcodes.co.uk/pb +2026-04-04T09:52:06.711807Z INFO property_map_server::pocketbase: PocketBase OAuth configured on users collection +2026-04-04T09:52:06.711837Z INFO property_map_server: Gemini configured (model: gemini-3-flash-preview) +2026-04-04T09:52:06.711856Z INFO property_map_server: Loading travel time data from /app/data/travel-times +2026-04-04T09:52:06.727911Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=2753 +2026-04-04T09:52:06.731050Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=2753 +2026-04-04T09:52:06.741344Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="car" destinations=2753 +2026-04-04T09:52:06.756414Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=2752 +2026-04-04T09:52:06.756459Z INFO property_map_server: Travel time store loaded modes=4 +2026-04-04T09:52:06.756524Z INFO property_map_server: Precomputed AI filters system prompt +2026-04-04T09:52:08.260112Z INFO property_map_server: All memory pages locked (mlockall) +2026-04-04T09:52:08.260151Z INFO property_map_server: Server listening on 0.0.0.0:8001 +2026-04-04T09:52:13.107969Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:52:13.109123Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:52:13.304460Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=52 cells_after_filter=48 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.4 json_ms=0.0 total_ms=0.5 +2026-04-04T09:52:13.547742Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=10805 filters=1 travel=0 total=8405 filters_raw="Listing status:Historical sale" ms=0.4 +2026-04-04T09:52:17.776220Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:52:17.776224Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:52:18.517199Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3403304 filters=4 travel=0 total=618194 filters_raw="Last known price:500000:inf;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" ms=35.5 +2026-04-04T09:52:19.574860Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=825 cells_after_filter=679 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=4 filters_raw="Last known price:500000:inf;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=8.1 json_ms=0.4 total_ms=8.5 +2026-04-04T09:52:19.849122Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=310268 filters=4 travel=0 total=90593 filters_raw="Last known price:500000:inf;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" ms=3.5 +2026-04-04T09:52:29.537989Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=52 cells_after_filter=48 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=2 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):0:535.7451" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.2 json_ms=0.0 total_ms=0.3 +2026-04-04T09:52:29.788060Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=10805 filters=2 travel=0 total=8405 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):0:535.7451" ms=0.1 +2026-04-04T09:52:31.760171Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=52 cells_after_filter=48 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.3 json_ms=0.0 total_ms=0.3 +2026-04-04T09:52:31.910715Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=52 cells_after_filter=48 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=2 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):0:535.7451" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.4 json_ms=0.1 total_ms=0.5 +2026-04-04T09:52:32.241961Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=52 cells_after_filter=48 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=2 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):36:535.7451" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.2 json_ms=0.0 total_ms=0.3 +2026-04-04T09:52:32.487304Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=10805 filters=2 travel=0 total=8405 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):36:535.7451" ms=0.2 +2026-04-04T09:52:33.740148Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=52 cells_after_filter=48 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.3 json_ms=0.0 total_ms=0.3 +2026-04-04T09:52:33.893888Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=52 cells_after_filter=48 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=2 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):36:535.7451" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.3 json_ms=0.0 total_ms=0.3 +2026-04-04T09:52:36.015267Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=52 cells_after_filter=48 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=2 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):49:535.7451" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.4 json_ms=0.0 total_ms=0.4 +2026-04-04T09:52:36.263117Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=10805 filters=2 travel=0 total=8405 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):49:535.7451" ms=0.1 +2026-04-04T09:52:39.308908Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=52 cells_after_filter=48 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=3 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):49:535.7451;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.2 json_ms=0.0 total_ms=0.3 +2026-04-04T09:52:39.553104Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=10805 filters=3 travel=0 total=8405 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):49:535.7451;;Good+ primary schools within 2km:0:49" ms=0.2 +2026-04-04T09:52:40.025321Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=52 cells_after_filter=48 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=2 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:0:49" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.3 json_ms=0.0 total_ms=0.3 +2026-04-04T09:52:40.177911Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=52 cells_after_filter=48 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=3 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):49:535.7451;;Good+ primary schools within 2km:0:49" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.3 json_ms=0.0 total_ms=0.3 +2026-04-04T09:52:40.491166Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=0 cells_after_filter=0 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=3 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):49:64;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.1 json_ms=0.0 total_ms=0.1 +2026-04-04T09:52:40.738811Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=10805 filters=3 travel=0 total=0 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):49:64;;Good+ primary schools within 2km:0:49" ms=0.1 +2026-04-04T09:52:41.152105Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=52 cells_after_filter=48 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=2 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:0:49" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.3 json_ms=0.0 total_ms=0.3 +2026-04-04T09:52:41.292605Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=0 cells_after_filter=0 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=3 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):49:64;;Good+ primary schools within 2km:0:49" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.2 json_ms=0.0 total_ms=0.2 +2026-04-04T09:52:41.474731Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=0 cells_after_filter=0 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=3 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.1 json_ms=0.0 total_ms=0.1 +2026-04-04T09:52:41.722392Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=10805 filters=3 travel=0 total=0 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:0:49" ms=0.1 +2026-04-04T09:52:45.316167Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=0 cells_after_filter=0 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=2 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.2 json_ms=0.0 total_ms=0.2 +2026-04-04T09:52:45.462502Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=0 cells_after_filter=0 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=3 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:0:49" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.1 json_ms=0.0 total_ms=0.1 +2026-04-04T09:52:46.611140Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=0 cells_after_filter=0 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=3 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:0:2" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.2 json_ms=0.0 total_ms=0.2 +2026-04-04T09:52:46.857291Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=10805 filters=3 travel=0 total=0 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:0:2" ms=0.1 +2026-04-04T09:52:48.579776Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=0 cells_after_filter=0 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=2 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.2 json_ms=0.0 total_ms=0.2 +2026-04-04T09:52:48.726580Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=0 cells_after_filter=0 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=3 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:0:2" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.2 json_ms=0.0 total_ms=0.2 +2026-04-04T09:52:48.923181Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=0 cells_after_filter=0 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=3 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:0:7" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.1 json_ms=0.0 total_ms=0.1 +2026-04-04T09:52:49.172286Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=10805 filters=3 travel=0 total=0 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:0:7" ms=0.1 +2026-04-04T09:52:50.383769Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=0 cells_after_filter=0 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=2 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.2 json_ms=0.0 total_ms=0.2 +2026-04-04T09:52:50.657802Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=0 cells_after_filter=0 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=3 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:0:9" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.1 json_ms=0.0 total_ms=0.1 +2026-04-04T09:52:50.908580Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=10805 filters=3 travel=0 total=0 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:0:9" ms=0.1 +2026-04-04T09:52:51.735856Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=0 cells_after_filter=0 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=2 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.2 json_ms=0.0 total_ms=0.2 +2026-04-04T09:52:52.024474Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=0 cells_after_filter=0 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=3 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.3 json_ms=0.0 total_ms=0.3 +2026-04-04T09:52:52.287937Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=10805 filters=3 travel=0 total=0 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:0:49" ms=0.2 +2026-04-04T09:52:55.588908Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=0 cells_after_filter=0 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=2 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.3 json_ms=0.0 total_ms=0.3 +2026-04-04T09:52:55.732332Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=0 cells_after_filter=0 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=3 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:0:49" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.2 json_ms=0.0 total_ms=0.2 +2026-04-04T09:52:56.471661Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=10805 parallel=false cells_before_filter=0 cells_after_filter=0 truncated=false bounds=51.5033,-0.1421,51.5143,-0.1163 filters=3 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:5:49" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.1 json_ms=0.0 total_ms=0.1 +2026-04-04T09:52:56.711090Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=10805 filters=3 travel=0 total=0 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:5:49" ms=0.1 +2026-04-04T09:52:57.939482Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=196 cells_after_filter=159 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=3 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:5:49" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.5 json_ms=0.1 total_ms=1.6 +2026-04-04T09:52:58.185654Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=138570 filters=3 travel=0 total=32010 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:5:49" ms=1.8 +2026-04-04T09:52:59.656552Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=196 cells_after_filter=159 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=2 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.8 json_ms=0.2 total_ms=2.0 +2026-04-04T09:52:59.947433Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=196 cells_after_filter=159 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=3 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:3:49" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.8 json_ms=0.1 total_ms=2.0 +2026-04-04T09:53:00.193546Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=138570 filters=3 travel=0 total=32010 filters_raw="Listing status:Historical sale;;Serious crime (avg/yr):35:64;;Good+ primary schools within 2km:3:49" ms=1.8 +2026-04-04T09:53:05.985267Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=399 cells_after_filter=341 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=2 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.6 json_ms=0.2 total_ms=1.9 +2026-04-04T09:53:06.237671Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=138570 filters=2 travel=0 total=112421 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49" ms=1.5 +2026-04-04T09:53:09.668444Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=399 cells_after_filter=341 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=3 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Education, Skills and Training Score:0:99.609" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.2 json_ms=0.2 total_ms=1.4 +2026-04-04T09:53:09.922342Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=138570 filters=3 travel=0 total=112421 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Education, Skills and Training Score:0:99.609" ms=1.5 +2026-04-04T09:53:10.539757Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=399 cells_after_filter=341 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=2 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.2 json_ms=0.3 total_ms=1.4 +2026-04-04T09:53:10.693514Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=399 cells_after_filter=341 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=3 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Education, Skills and Training Score:0:99.609" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.2 total_ms=1.5 +2026-04-04T09:53:10.912921Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=383 cells_after_filter=329 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=3 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Education, Skills and Training Score:82.9:99.609" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.2 total_ms=1.5 +2026-04-04T09:53:11.162011Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=138570 filters=3 travel=0 total=97636 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Education, Skills and Training Score:82.9:99.609" ms=1.6 +2026-04-04T09:53:13.646055Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=383 cells_after_filter=329 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=4 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Education, Skills and Training Score:82.9:99.609;;Good+ secondary schools within 2km:0:18" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.2 json_ms=0.2 total_ms=1.4 +2026-04-04T09:53:13.890592Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=138570 filters=4 travel=0 total=97636 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Education, Skills and Training Score:82.9:99.609;;Good+ secondary schools within 2km:0:18" ms=1.8 +2026-04-04T09:53:15.774963Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=383 cells_after_filter=329 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=3 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Education, Skills and Training Score:82.9:99.609" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.4 json_ms=0.2 total_ms=1.6 +2026-04-04T09:53:15.917610Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=383 cells_after_filter=329 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=4 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Education, Skills and Training Score:82.9:99.609;;Good+ secondary schools within 2km:0:18" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.2 json_ms=0.2 total_ms=1.5 +2026-04-04T09:53:16.244314Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=383 cells_after_filter=329 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=4 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Education, Skills and Training Score:82.9:99.609;;Good+ secondary schools within 2km:0:18" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.2 total_ms=1.5 +2026-04-04T09:53:16.482315Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=138570 filters=4 travel=0 total=97636 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Education, Skills and Training Score:82.9:99.609;;Good+ secondary schools within 2km:0:18" ms=1.9 +2026-04-04T09:53:16.757756Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=383 cells_after_filter=329 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=3 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Education, Skills and Training Score:82.9:99.609" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.1 json_ms=0.3 total_ms=1.4 +2026-04-04T09:53:16.903127Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=383 cells_after_filter=329 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=4 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Education, Skills and Training Score:82.9:99.609;;Good+ secondary schools within 2km:0:18" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.2 json_ms=0.3 total_ms=1.5 +2026-04-04T09:53:17.198701Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=383 cells_after_filter=329 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=4 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Education, Skills and Training Score:82.9:99.609;;Good+ secondary schools within 2km:1:18" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.2 json_ms=0.2 total_ms=1.5 +2026-04-04T09:53:17.446671Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=138570 filters=4 travel=0 total=97636 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Education, Skills and Training Score:82.9:99.609;;Good+ secondary schools within 2km:1:18" ms=1.9 +2026-04-04T09:54:06.836832Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=825 cells_after_filter=679 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=4 filters_raw="Last known price:500000:inf;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.1 json_ms=0.5 total_ms=2.6 +2026-04-04T09:54:07.112902Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=310268 filters=4 travel=0 total=90593 filters_raw="Last known price:500000:inf;;Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" ms=3.3 +2026-04-04T09:55:34.040391Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=844 cells_after_filter=685 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=3 filters_raw="Price per sqm:12:30070.744;;Estimated monthly rent:300:7750;;Good+ primary schools within 2km:0:49" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.1 json_ms=0.4 total_ms=2.5 +2026-04-04T09:55:34.584934Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=879 cells_after_filter=711 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.6 json_ms=0.6 total_ms=2.2 +2026-04-04T09:55:41.928304Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=853 cells_after_filter=692 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=1 filters_raw="Former council house:Yes|No" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.9 json_ms=0.4 total_ms=3.3 +2026-04-04T09:55:42.186681Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=310268 filters=1 travel=0 total=221385 filters_raw="Former council house:Yes|No" ms=9.5 +2026-04-04T09:55:43.463209Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=771 cells_after_filter=635 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=1 filters_raw="Former council house:Yes" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.0 json_ms=0.3 total_ms=2.4 +2026-04-04T09:55:43.737057Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=310268 filters=1 travel=0 total=8802 filters_raw="Former council house:Yes" ms=9.4 +2026-04-04T09:55:44.388585Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=89195da4b93ffff resolution=9 total_count=46 filters=1 filters_raw="Former council house:Yes" ms=0.2 +2026-04-04T09:55:44.598274Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=267143 parallel=true cells_before_filter=663 cells_after_filter=517 truncated=false bounds=51.4896,-0.1877,51.5404,-0.0723 filters=1 filters_raw="Former council house:Yes" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.8 json_ms=0.3 total_ms=2.1 +2026-04-04T09:55:44.837518Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=267143 filters=1 travel=0 total=7227 filters_raw="Former council house:Yes" ms=7.4 +2026-04-04T09:55:45.943713Z INFO property_map_server::routes::properties: GET /api/hexagon-properties h3=89195da4b93ffff resolution=9 total=46 returned=46 offset=0 filters=1 filters_raw="Former council house:Yes" ms=0.4 +2026-04-04T09:55:53.228291Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=538838 parallel=true cells_before_filter=1545 cells_after_filter=1438 truncated=false bounds=51.4691,-0.2443,51.5576,-0.0437 filters=1 filters_raw="Former council house:Yes" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=3.4 json_ms=0.9 total_ms=4.3 +2026-04-04T09:55:54.139826Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=1286389 filters=1 travel=0 total=39191 filters_raw="Former council house:Yes" ms=38.6 +2026-04-04T09:55:55.294889Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=267143 parallel=true cells_before_filter=663 cells_after_filter=517 truncated=false bounds=51.4896,-0.1877,51.5404,-0.0723 filters=1 filters_raw="Former council house:Yes" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.1 json_ms=0.3 total_ms=2.4 +2026-04-04T09:55:55.608290Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=267143 filters=1 travel=0 total=7227 filters_raw="Former council house:Yes" ms=8.1 +2026-04-04T09:56:54.660904Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=383 cells_after_filter=329 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=3 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Education, Skills and Training Score:82.9:99.609" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.3 total_ms=1.6 +2026-04-04T09:56:54.809750Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=383 cells_after_filter=329 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=4 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Education, Skills and Training Score:82.9:99.609;;Good+ secondary schools within 2km:1:18" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.4 json_ms=0.2 total_ms=1.7 +2026-04-04T09:57:04.149232Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=399 cells_after_filter=341 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=2 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.5 json_ms=0.3 total_ms=1.8 +2026-04-04T09:57:04.303078Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=399 cells_after_filter=341 truncated=false bounds=51.4947,-0.1740,51.5324,-0.0851 filters=3 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Good+ secondary schools within 2km:1:18" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.3 total_ms=1.6 +2026-04-04T09:57:04.549588Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=138570 filters=3 travel=0 total=112421 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Good+ secondary schools within 2km:1:18" ms=2.0 +2026-04-04T09:57:17.202032Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T09:57:17.202686Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T09:57:17.440798Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=399 cells_after_filter=346 truncated=false bounds=51.4945,-0.1746,51.5327,-0.0844 filters=3 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Good+ secondary schools within 2km:1:18" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.5 json_ms=0.2 total_ms=1.7 +2026-04-04T09:57:17.691060Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=138570 filters=3 travel=0 total=112421 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Good+ secondary schools within 2km:1:18" ms=1.7 +2026-04-04T09:57:18.786724Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=399 cells_after_filter=346 truncated=false bounds=51.4945,-0.1746,51.5327,-0.0844 filters=3 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Good+ secondary schools within 2km:1:18" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.4 json_ms=0.3 total_ms=1.7 +2026-04-04T09:57:19.792385Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=399 cells_after_filter=346 truncated=false bounds=51.4945,-0.1746,51.5327,-0.0844 filters=3 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Good+ secondary schools within 2km:1:18" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.5 json_ms=0.3 total_ms=1.8 +2026-04-04T09:57:20.703555Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=399 cells_after_filter=346 truncated=false bounds=51.4945,-0.1746,51.5327,-0.0844 filters=3 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Good+ secondary schools within 2km:1:18" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.4 json_ms=0.2 total_ms=1.6 +2026-04-04T09:57:23.441270Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=399 cells_after_filter=346 truncated=false bounds=51.4945,-0.1746,51.5327,-0.0844 filters=2 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.4 json_ms=0.3 total_ms=1.6 +2026-04-04T09:57:23.587427Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=399 cells_after_filter=346 truncated=false bounds=51.4945,-0.1746,51.5327,-0.0844 filters=3 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Good+ secondary schools within 2km:1:18" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.3 total_ms=1.5 +2026-04-04T09:57:23.802126Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=393 cells_after_filter=340 truncated=false bounds=51.4945,-0.1746,51.5327,-0.0844 filters=3 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Good+ secondary schools within 2km:2:18" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.6 json_ms=0.2 total_ms=1.8 +2026-04-04T09:57:24.051097Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=138570 filters=3 travel=0 total=107687 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Good+ secondary schools within 2km:2:18" ms=1.8 +2026-04-04T09:57:25.937699Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=393 cells_after_filter=340 truncated=false bounds=51.4945,-0.1746,51.5327,-0.0844 filters=2 filters_raw="Listing status:Historical sale;;Good+ secondary schools within 2km:2:18" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.5 json_ms=0.2 total_ms=1.7 +2026-04-04T09:57:26.080544Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=393 cells_after_filter=340 truncated=false bounds=51.4945,-0.1746,51.5327,-0.0844 filters=3 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:3:49;;Good+ secondary schools within 2km:2:18" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.3 total_ms=1.6 +2026-04-04T09:57:26.390697Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=393 cells_after_filter=340 truncated=false bounds=51.4945,-0.1746,51.5327,-0.0844 filters=3 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:7:49;;Good+ secondary schools within 2km:2:18" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.5 json_ms=0.2 total_ms=1.7 +2026-04-04T09:57:26.641621Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=138570 filters=3 travel=0 total=107670 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:7:49;;Good+ secondary schools within 2km:2:18" ms=1.7 +2026-04-04T09:57:28.175318Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=393 cells_after_filter=340 truncated=false bounds=51.4945,-0.1746,51.5327,-0.0844 filters=2 filters_raw="Listing status:Historical sale;;Good+ secondary schools within 2km:2:18" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.3 total_ms=1.5 +2026-04-04T09:57:28.457899Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=389 cells_after_filter=336 truncated=false bounds=51.4945,-0.1746,51.5327,-0.0844 filters=3 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:10:49;;Good+ secondary schools within 2km:2:18" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.2 total_ms=1.5 +2026-04-04T09:57:28.701294Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=138570 filters=3 travel=0 total=106656 filters_raw="Listing status:Historical sale;;Good+ primary schools within 2km:10:49;;Good+ secondary schools within 2km:2:18" ms=1.7 +2026-04-04T09:57:30.569829Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=393 cells_after_filter=340 truncated=false bounds=51.4945,-0.1746,51.5327,-0.0844 filters=2 filters_raw="Listing status:Historical sale;;Good+ secondary schools within 2km:2:18" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.2 total_ms=1.5 +2026-04-04T09:57:30.805473Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=138570 filters=2 travel=0 total=107687 filters_raw="Listing status:Historical sale;;Good+ secondary schools within 2km:2:18" ms=1.5 +2026-04-04T09:57:32.404849Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=399 cells_after_filter=346 truncated=false bounds=51.4945,-0.1746,51.5327,-0.0844 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.2 json_ms=0.2 total_ms=1.4 +2026-04-04T09:57:32.655967Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=138570 filters=1 travel=0 total=112421 filters_raw="Listing status:Historical sale" ms=4.2 +2026-04-04T09:57:37.029036Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=138570 parallel=true cells_before_filter=399 cells_after_filter=304 truncated=false bounds=51.4972,-0.1746,51.5300,-0.0844 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.2 total_ms=1.5 +2026-04-04T09:57:37.277670Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=138570 filters=1 travel=0 total=112421 filters_raw="Listing status:Historical sale" ms=4.0 +2026-04-04T09:57:38.013138Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=223855 parallel=true cells_before_filter=647 cells_after_filter=561 truncated=false bounds=51.4800,-0.1746,51.5472,-0.0844 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.4 json_ms=0.4 total_ms=2.7 +2026-04-04T09:57:38.264644Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=223855 filters=1 travel=0 total=185222 filters_raw="Listing status:Historical sale" ms=6.9 +2026-04-04T09:57:39.240772Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=172534 parallel=true cells_before_filter=475 cells_after_filter=409 truncated=false bounds=51.4898,-0.1746,51.5374,-0.0844 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.6 json_ms=0.2 total_ms=1.8 +2026-04-04T09:57:39.498901Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=172534 filters=1 travel=0 total=139912 filters_raw="Listing status:Historical sale" ms=5.2 +2026-04-04T09:57:42.717623Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=223855 parallel=true cells_before_filter=647 cells_after_filter=577 truncated=false bounds=51.4781,-0.1746,51.5491,-0.0844 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.0 json_ms=0.3 total_ms=2.3 +2026-04-04T09:57:42.969460Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=223855 filters=1 travel=0 total=185222 filters_raw="Listing status:Historical sale" ms=6.9 +2026-04-04T09:57:44.682704Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=223855 parallel=true cells_before_filter=647 cells_after_filter=577 truncated=false bounds=51.4781,-0.1746,51.5491,-0.0844 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.9 json_ms=0.3 total_ms=2.2 +2026-04-04T09:57:44.937421Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=223855 filters=1 travel=0 total=185222 filters_raw="Listing status:Historical sale" ms=6.8 +2026-04-04T09:57:46.684676Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=75509 parallel=true cells_before_filter=235 cells_after_filter=189 truncated=false bounds=51.5052,-0.1746,51.5220,-0.0844 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.0 json_ms=0.1 total_ms=1.1 +2026-04-04T09:57:46.937811Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=75509 filters=1 travel=0 total=60159 filters_raw="Listing status:Historical sale" ms=2.4 +2026-04-04T09:57:47.784802Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=223855 parallel=true cells_before_filter=647 cells_after_filter=577 truncated=false bounds=51.4781,-0.1746,51.5491,-0.0844 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.7 json_ms=0.3 total_ms=2.0 +2026-04-04T09:57:50.373082Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=75509 parallel=true cells_before_filter=235 cells_after_filter=189 truncated=false bounds=51.5052,-0.1746,51.5220,-0.0844 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.0 json_ms=0.1 total_ms=1.1 +2026-04-04T09:57:51.219922Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=563 cells_after_filter=511 truncated=false bounds=51.4835,-0.1746,51.5437,-0.0844 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.8 json_ms=0.3 total_ms=2.1 +2026-04-04T09:57:51.473666Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=199182 filters=1 travel=0 total=163447 filters_raw="Listing status:Historical sale" ms=6.1 +2026-04-04T16:39:54.658472Z INFO property_map_server: Prometheus metrics initialized +2026-04-04T16:39:54.658668Z INFO property_map_server: Loading property data from /app/data/properties.parquet, /app/data/postcode.parquet, /app/data-scraped/online_listings_buy.parquet, /app/data-scraped/online_listings_rent.parquet +2026-04-04T16:39:54.658678Z INFO property_map_server::data::property: Loading postcode features from "/app/data/postcode.parquet" +2026-04-04T16:39:54.812468Z INFO property_map_server::data::property: Postcode features loaded rows=1262364 +2026-04-04T16:39:54.812480Z INFO property_map_server::data::property: Loading properties from "/app/data/properties.parquet" +2026-04-04T16:39:57.851031Z INFO property_map_server::data::property: Properties joined with postcodes rows=15203393 +2026-04-04T16:39:57.851044Z INFO property_map_server::data::property: Loading buy listings from "/app/data-scraped/online_listings_buy.parquet" +2026-04-04T16:39:58.333504Z INFO property_map_server::data::property: buy listings joined rows=1060501 +2026-04-04T16:39:58.333527Z INFO property_map_server::data::property: Loading rent listings from "/app/data-scraped/online_listings_rent.parquet" +2026-04-04T16:39:58.466642Z INFO property_map_server::data::property: rent listings joined rows=266585 +2026-04-04T16:39:58.466651Z INFO property_map_server::data::property: Concatenating all data sources +2026-04-04T16:40:03.801219Z INFO property_map_server::data::property: All data sources combined properties=15203393 buy_listings=1060501 rent_listings=266585 total=16530479 +2026-04-04T16:40:03.801296Z INFO property_map_server::data::property: Feature columns from config numeric=58 enums=7 total=65 +2026-04-04T16:40:05.224951Z INFO property_map_server::data::property: Combined data selected rows=16530479 +2026-04-04T16:40:05.384825Z INFO property_map_server::data::property: Extracting numeric feature columns +2026-04-04T16:40:05.800951Z INFO property_map_server::data::property: Computing histograms for numeric features +2026-04-04T16:40:07.052140Z INFO property_map_server::data::property: Extracting string columns +2026-04-04T16:40:09.411155Z INFO property_map_server::data::property: Building enum features +2026-04-04T16:40:10.729165Z INFO property_map_server::data::property: Extracting renovation history +2026-04-04T16:40:12.827870Z INFO property_map_server::data::property: Renovation history extracted properties_with_events=1829784 +2026-04-04T16:40:12.827881Z INFO property_map_server::data::property: Extracting listing features +2026-04-04T16:40:13.522059Z INFO property_map_server::data::property: Listing features extracted properties_with_features=527541 +2026-04-04T16:40:13.522068Z INFO property_map_server::data::property: Sorting rows by spatial locality +2026-04-04T16:40:14.411754Z INFO property_map_server::data::property: Building interned strings +2026-04-04T16:40:20.776006Z INFO property_map_server::data::property: Transposing to row-major layout (spatially sorted, quantized to u16) +2026-04-04T16:40:23.481346Z INFO property_map_server::data::property: Data loading complete +2026-04-04T16:40:25.167424Z INFO property_map_server: Property data loaded rows=16530479 features=65 enums=7 +2026-04-04T16:40:25.167433Z INFO property_map_server: Building spatial grid index (0.01° cells) +2026-04-04T16:40:25.265061Z INFO property_map_server: Precomputing H3 cells at resolution 12 +2026-04-04T16:40:25.265071Z INFO property_map_server::data::property: Precomputing H3 cells at resolution 12 +2026-04-04T16:40:25.670091Z INFO property_map_server::data::property: H3 precomputation complete (16530479 cells) +2026-04-04T16:40:25.670114Z INFO property_map_server: Loading POI data from /app/data/filtered_uk_pois.parquet +2026-04-04T16:40:25.670120Z INFO property_map_server::data::poi: Loading POI data from "/app/data/filtered_uk_pois.parquet"... +2026-04-04T16:40:25.691903Z INFO property_map_server::data::poi: Loaded 550611 POIs +2026-04-04T16:40:25.786559Z INFO property_map_server::data::poi: POI string columns interned category_unique=74 group_unique=11 emoji_unique=71 +2026-04-04T16:40:25.787027Z INFO property_map_server::data::poi: POI data loading complete. +2026-04-04T16:40:25.815918Z INFO property_map_server: POI data loaded pois=550611 +2026-04-04T16:40:25.815928Z INFO property_map_server: Building POI spatial grid index +2026-04-04T16:40:25.820658Z INFO property_map_server: Loading place data from /app/data/places.parquet +2026-04-04T16:40:25.820667Z INFO property_map_server::data::places: Loading place data from "/app/data/places.parquet"... +2026-04-04T16:40:25.822625Z INFO property_map_server::data::places: Loaded 3474 places +2026-04-04T16:40:25.823340Z INFO property_map_server::data::places: Place data loaded places=3474 types=2 with_population=71 with_city=3392 +2026-04-04T16:40:25.823405Z INFO property_map_server: Place data loaded places=3474 +2026-04-04T16:40:25.823413Z INFO property_map_server: Loading postcode boundaries from /app/data/postcode_boundaries +2026-04-04T16:40:25.823416Z INFO property_map_server::data::postcodes: Loading postcode boundaries from "/app/data/postcode_boundaries" +2026-04-04T16:40:25.826692Z INFO property_map_server::data::postcodes: Found GeoJSON files to process files=2361 +2026-04-04T16:40:33.217429Z INFO property_map_server::data::postcodes: Postcode boundary data ready postcodes=1490140 +2026-04-04T16:40:33.453668Z INFO property_map_server: Postcode boundaries loaded postcodes=1490140 +2026-04-04T16:40:33.594039Z INFO property_map_server::data::postcodes: Outcode data derived from postcodes outcodes=2361 +2026-04-04T16:40:33.594099Z INFO property_map_server: Loading PMTiles from /app/data/uk.pmtiles +2026-04-04T16:40:33.598517Z INFO property_map_server: PMTiles loaded successfully +2026-04-04T16:40:33.633335Z INFO property_map_server: No --dist provided; static serving and OG injection disabled +2026-04-04T16:40:33.664761Z INFO property_map_server: Screenshot service configured: http://screenshot:8002 +2026-04-04T16:40:33.664816Z INFO property_map_server: Precomputed features response groups=7 +2026-04-04T16:40:33.664833Z INFO property_map_server: PocketBase configured: http://pocketbase:8090 +2026-04-04T16:40:33.731537Z INFO property_map_server::pocketbase: PocketBase users collection already has all required fields +2026-04-04T16:40:33.734637Z INFO property_map_server::pocketbase: PocketBase collection 'saved_searches' API rules updated +2026-04-04T16:40:33.738612Z INFO property_map_server::pocketbase: PocketBase collection 'saved_properties' API rules updated +2026-04-04T16:40:33.786159Z INFO property_map_server::pocketbase: PocketBase meta.appURL set to https://perfect-postcodes.co.uk/pb +2026-04-04T16:40:33.789573Z INFO property_map_server::pocketbase: PocketBase OAuth configured on users collection +2026-04-04T16:40:33.789588Z INFO property_map_server: Gemini configured (model: gemini-3-flash-preview) +2026-04-04T16:40:33.789601Z INFO property_map_server: Loading travel time data from /app/data/travel-times +2026-04-04T16:40:33.796514Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="bicycle" destinations=2753 +2026-04-04T16:40:33.803024Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="walking" destinations=2753 +2026-04-04T16:40:33.809734Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="car" destinations=2753 +2026-04-04T16:40:33.814443Z INFO property_map_server::data::travel_time: Travel time mode discovered mode="transit" destinations=2752 +2026-04-04T16:40:33.814462Z INFO property_map_server: Travel time store loaded modes=4 +2026-04-04T16:40:33.814505Z INFO property_map_server: Precomputed AI filters system prompt +2026-04-04T16:40:38.841370Z INFO property_map_server: All memory pages locked (mlockall) +2026-04-04T16:40:38.841413Z INFO property_map_server: Server listening on 0.0.0.0:8001 +2026-04-04T16:45:08.608456Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T16:45:08.608463Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T16:45:08.920241Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=310268 parallel=true cells_before_filter=771 cells_after_filter=635 truncated=false bounds=51.4896,-0.2000,51.5404,-0.0600 filters=1 filters_raw="Former council house:Yes" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=7.0 json_ms=0.4 total_ms=7.4 +2026-04-04T16:45:09.177392Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=310268 filters=1 travel=0 total=8802 filters_raw="Former council house:Yes" ms=9.1 +2026-04-04T16:45:26.974416Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T16:45:26.975311Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T16:45:41.467910Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=435543 parallel=true cells_before_filter=1322 cells_after_filter=1268 truncated=false bounds=51.4729,-0.2093,51.5624,-0.0533 filters=0 filters_raw="-" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=5.1 json_ms=0.8 total_ms=5.9 +2026-04-04T16:45:44.805432Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T16:45:44.805447Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T16:45:45.124965Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=435543 parallel=true cells_before_filter=1295 cells_after_filter=1236 truncated=false bounds=51.4733,-0.2085,51.5619,-0.0541 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=4.6 json_ms=0.8 total_ms=5.3 +2026-04-04T16:45:45.340277Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=435543 filters=1 travel=0 total=370010 filters_raw="Listing status:Historical sale" ms=12.8 +2026-04-04T16:45:55.770809Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=435543 parallel=true cells_before_filter=1295 cells_after_filter=1236 truncated=false bounds=51.4733,-0.2085,51.5619,-0.0541 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=3.4 json_ms=1.0 total_ms=4.4 +2026-04-04T16:45:56.902873Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=861141 parallel=true cells_before_filter=3022 cells_after_filter=2854 truncated=false bounds=51.4453,-0.2435,51.5847,-0.0006 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=7.2 json_ms=2.5 total_ms=9.7 +2026-04-04T16:45:57.144730Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=861141 filters=1 travel=0 total=750424 filters_raw="Listing status:Historical sale" ms=26.4 +2026-04-04T16:45:58.647828Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=234961 parallel=true cells_before_filter=690 cells_after_filter=599 truncated=false bounds=51.4918,-0.1847,51.5522,-0.0794 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=2.1 json_ms=0.5 total_ms=2.6 +2026-04-04T16:45:58.896370Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=234961 filters=1 travel=0 total=196263 filters_raw="Listing status:Historical sale" ms=7.1 +2026-04-04T16:45:59.568596Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=81076 parallel=true cells_before_filter=279 cells_after_filter=221 truncated=false bounds=51.5074,-0.1649,51.5412,-0.1060 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.0 json_ms=0.2 total_ms=1.2 +2026-04-04T16:45:59.821068Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=81076 filters=1 travel=0 total=65545 filters_raw="Listing status:Historical sale" ms=2.9 +2026-04-04T16:46:05.189591Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=81076 parallel=true cells_before_filter=279 cells_after_filter=221 truncated=false bounds=51.5074,-0.1649,51.5412,-0.1060 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.1 json_ms=0.2 total_ms=1.3 +2026-04-04T16:46:05.883781Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=81076 parallel=true cells_before_filter=279 cells_after_filter=221 truncated=false bounds=51.5074,-0.1649,51.5412,-0.1060 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.9 json_ms=0.2 total_ms=1.1 +2026-04-04T16:46:07.800636Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=256335 parallel=true cells_before_filter=750 cells_after_filter=698 truncated=false bounds=51.4905,-0.1849,51.5568,-0.0693 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=2.2 json_ms=0.6 total_ms=2.8 +2026-04-04T16:46:08.060843Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=256335 filters=1 travel=0 total=215003 filters_raw="Listing status:Historical sale" ms=7.8 +2026-04-04T16:47:26.186916Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=256335 parallel=true cells_before_filter=750 cells_after_filter=698 truncated=false bounds=51.4905,-0.1849,51.5568,-0.0693 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=2.5 json_ms=0.6 total_ms=3.1 +2026-04-04T16:47:26.450203Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=256335 filters=1 travel=0 total=215003 filters_raw="Listing status:Historical sale" ms=7.9 +2026-04-04T16:47:28.349057Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T16:47:28.349059Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T16:47:28.651152Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=256335 parallel=true cells_before_filter=750 cells_after_filter=706 truncated=false bounds=51.4900,-0.1856,51.5571,-0.0686 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.2 json_ms=0.4 total_ms=2.6 +2026-04-04T16:47:28.902368Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=256335 filters=1 travel=0 total=215003 filters_raw="Listing status:Historical sale" ms=7.8 +2026-04-04T16:47:33.552744Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=256335 parallel=true cells_before_filter=750 cells_after_filter=706 truncated=false bounds=51.4900,-0.1856,51.5571,-0.0686 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.1 json_ms=0.3 total_ms=2.5 +2026-04-04T16:47:33.809693Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=256335 filters=1 travel=0 total=215003 filters_raw="Listing status:Historical sale" ms=7.4 +2026-04-04T16:47:39.079308Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T16:47:39.079311Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T16:49:13.301599Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T16:49:13.301607Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T16:49:13.679867Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=256335 parallel=true cells_before_filter=750 cells_after_filter=706 truncated=false bounds=51.4900,-0.1856,51.5571,-0.0686 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.2 json_ms=0.4 total_ms=2.5 +2026-04-04T16:49:13.841398Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=256335 filters=1 travel=0 total=215003 filters_raw="Listing status:Historical sale" ms=7.5 +2026-04-04T16:57:13.826311Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T16:57:13.826313Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T16:57:14.213065Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=256335 parallel=true cells_before_filter=750 cells_after_filter=706 truncated=false bounds=51.4900,-0.1856,51.5571,-0.0686 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.2 json_ms=0.4 total_ms=2.6 +2026-04-04T16:57:14.441819Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=256335 filters=1 travel=0 total=215003 filters_raw="Listing status:Historical sale" ms=7.6 +2026-04-04T17:33:13.797118Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=256335 parallel=true cells_before_filter=750 cells_after_filter=706 truncated=false bounds=51.4900,-0.1856,51.5571,-0.0686 filters=1 filters_raw="Listing status:Historical sale" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=2.1 json_ms=0.6 total_ms=2.7 +2026-04-04T17:33:20.909853Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T17:33:20.910876Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T17:33:38.921153Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T17:33:38.922096Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T17:34:01.172545Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=580 cells_after_filter=457 truncated=false bounds=51.4896,-0.1743,51.5404,-0.0857 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.8 json_ms=0.3 total_ms=2.1 +2026-04-04T17:34:01.420765Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=199182 filters=1 travel=0 total=199182 filters_raw="Listing status:Historical sale|For sale|For rent" ms=5.7 +2026-04-04T17:34:03.085721Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=580 cells_after_filter=457 truncated=false bounds=51.4896,-0.1743,51.5404,-0.0857 filters=2 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.6 json_ms=0.2 total_ms=1.9 +2026-04-04T17:34:03.386412Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=199182 filters=2 travel=0 total=199182 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other" ms=4.3 +2026-04-04T17:34:03.702911Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=575 cells_after_filter=454 truncated=false bounds=51.4896,-0.1743,51.5404,-0.0857 filters=3 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.9 json_ms=0.3 total_ms=2.1 +2026-04-04T17:34:04.096076Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=538 cells_after_filter=433 truncated=false bounds=51.4896,-0.1743,51.5404,-0.0857 filters=4 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.3 total_ms=1.6 +2026-04-04T17:34:04.475772Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=520 cells_after_filter=420 truncated=false bounds=51.4896,-0.1743,51.5404,-0.0857 filters=5 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.2 total_ms=1.6 +2026-04-04T17:34:04.725663Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=199182 filters=5 travel=0 total=7158 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" ms=1.4 +2026-04-04T17:34:07.655205Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=520 cells_after_filter=420 truncated=false bounds=51.4896,-0.1743,51.5404,-0.0857 filters=5 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.3 total_ms=1.6 +2026-04-04T17:34:17.331607Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T17:34:17.332627Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T17:34:17.693823Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=520 cells_after_filter=420 truncated=false bounds=51.4896,-0.1743,51.5404,-0.0857 filters=5 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.2 total_ms=1.6 +2026-04-04T17:34:17.910666Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=199182 filters=5 travel=0 total=7158 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" ms=1.2 +2026-04-04T17:34:19.108010Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=520 cells_after_filter=420 truncated=false bounds=51.4896,-0.1743,51.5404,-0.0857 filters=5 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.3 json_ms=0.3 total_ms=1.6 +2026-04-04T17:39:40.688252Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T17:39:40.688255Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T17:39:41.010461Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=520 cells_after_filter=420 truncated=false bounds=51.4896,-0.1743,51.5404,-0.0857 filters=5 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.4 json_ms=0.2 total_ms=1.6 +2026-04-04T17:39:41.296845Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=199182 filters=5 travel=0 total=7158 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" ms=1.4 +2026-04-04T17:39:42.509600Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=520 cells_after_filter=420 truncated=false bounds=51.4896,-0.1743,51.5404,-0.0857 filters=5 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.4 json_ms=0.3 total_ms=1.8 +2026-04-04T17:39:44.239048Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=696197 parallel=true cells_before_filter=1951 cells_after_filter=1879 truncated=false bounds=51.4548,-0.2169,51.5734,-0.0103 filters=5 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=4.0 json_ms=1.4 total_ms=5.4 +2026-04-04T17:39:44.486522Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=696197 filters=5 travel=0 total=17150 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" ms=3.6 +2026-04-04T17:39:45.324769Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=1674796 filters=5 travel=0 total=30076 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" ms=10.6 +2026-04-04T17:39:48.176839Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=520 cells_after_filter=420 truncated=false bounds=51.4896,-0.1743,51.5404,-0.0857 filters=5 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.5 json_ms=0.3 total_ms=1.8 +2026-04-04T17:39:48.461691Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=199182 filters=5 travel=0 total=7158 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" ms=1.4 +2026-04-04T17:39:50.099685Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=62591 parallel=true cells_before_filter=205 cells_after_filter=173 truncated=false bounds=51.5007,-0.1629,51.5329,-0.1068 filters=5 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.8 json_ms=0.1 total_ms=0.9 +2026-04-04T17:39:50.360489Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=62591 filters=5 travel=0 total=2739 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" ms=0.4 +2026-04-04T17:39:53.467402Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=62591 parallel=true cells_before_filter=208 cells_after_filter=175 truncated=false bounds=51.5007,-0.1629,51.5329,-0.1068 filters=4 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Asking price:1:inf;;Asking price per sqm:0:49347.58" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.6 json_ms=0.1 total_ms=0.8 +2026-04-04T17:39:53.721076Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=62591 filters=4 travel=0 total=3464 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Asking price:1:inf;;Asking price per sqm:0:49347.58" ms=0.5 +2026-04-04T17:39:54.747753Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=62591 parallel=true cells_before_filter=208 cells_after_filter=175 truncated=false bounds=51.5007,-0.1629,51.5329,-0.1068 filters=3 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Asking price per sqm:0:49347.58" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.7 json_ms=0.1 total_ms=0.8 +2026-04-04T17:39:54.995568Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=62591 filters=3 travel=0 total=3464 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Asking price per sqm:0:49347.58" ms=0.8 +2026-04-04T17:39:55.634572Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=62591 parallel=true cells_before_filter=229 cells_after_filter=190 truncated=false bounds=51.5007,-0.1629,51.5329,-0.1068 filters=2 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=0.9 json_ms=0.2 total_ms=1.1 +2026-04-04T17:39:55.888482Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=62591 filters=2 travel=0 total=62591 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other" ms=0.9 +2026-04-04T17:39:57.262537Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=62591 parallel=true cells_before_filter=229 cells_after_filter=190 truncated=false bounds=51.5007,-0.1629,51.5329,-0.1068 filters=2 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.9 json_ms=0.1 total_ms=1.0 +2026-04-04T17:39:57.936201Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=62591 parallel=true cells_before_filter=229 cells_after_filter=190 truncated=false bounds=51.5007,-0.1629,51.5329,-0.1068 filters=2 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.0 json_ms=0.2 total_ms=1.1 +2026-04-04T17:40:28.881567Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T17:40:28.881783Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T17:40:30.310586Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=24630 parallel=false cells_before_filter=101 cells_after_filter=57 truncated=false bounds=51.5095,-0.1429,51.5205,-0.1171 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.1 json_ms=0.1 total_ms=1.1 +2026-04-04T17:40:30.566156Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=24630 filters=1 travel=0 total=18876 filters_raw="Listing status:Historical sale" ms=0.8 +2026-04-04T17:40:31.885745Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=24630 parallel=false cells_before_filter=101 cells_after_filter=80 truncated=false bounds=51.5048,-0.1429,51.5252,-0.1171 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.1 json_ms=0.1 total_ms=1.2 +2026-04-04T17:40:32.135311Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=24630 filters=1 travel=0 total=18876 filters_raw="Listing status:Historical sale" ms=0.7 +2026-04-04T17:40:34.218609Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=24630 parallel=false cells_before_filter=101 cells_after_filter=69 truncated=false bounds=51.5072,-0.1429,51.5228,-0.1171 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.1 json_ms=0.1 total_ms=1.2 +2026-04-04T17:40:34.473260Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=24630 filters=1 travel=0 total=18876 filters_raw="Listing status:Historical sale" ms=0.8 +2026-04-04T17:40:37.046020Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=24630 parallel=false cells_before_filter=101 cells_after_filter=70 truncated=false bounds=51.5069,-0.1429,51.5231,-0.1171 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.0 json_ms=0.1 total_ms=1.1 +2026-04-04T17:40:37.289616Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=24630 filters=1 travel=0 total=18876 filters_raw="Listing status:Historical sale" ms=0.8 +2026-04-04T17:42:08.253043Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=62591 parallel=true cells_before_filter=229 cells_after_filter=190 truncated=false bounds=51.5007,-0.1629,51.5329,-0.1068 filters=2 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.0 json_ms=0.2 total_ms=1.2 +2026-04-04T17:42:08.318527Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=24630 parallel=false cells_before_filter=101 cells_after_filter=70 truncated=false bounds=51.5069,-0.1429,51.5231,-0.1171 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.2 json_ms=0.0 total_ms=1.2 +2026-04-04T17:42:08.497017Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=62591 filters=2 travel=0 total=62591 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other" ms=1.0 +2026-04-04T17:42:08.570904Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=24630 filters=1 travel=0 total=18876 filters_raw="Listing status:Historical sale" ms=0.7 +2026-04-04T17:42:13.334753Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=24630 parallel=false cells_before_filter=101 cells_after_filter=70 truncated=false bounds=51.5069,-0.1429,51.5231,-0.1171 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.1 json_ms=0.1 total_ms=1.1 +2026-04-04T17:42:13.587373Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=24630 filters=1 travel=0 total=18876 filters_raw="Listing status:Historical sale" ms=0.8 +2026-04-04T17:42:13.724677Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=62591 parallel=true cells_before_filter=229 cells_after_filter=190 truncated=false bounds=51.5007,-0.1629,51.5329,-0.1068 filters=2 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.0 json_ms=0.2 total_ms=1.2 +2026-04-04T17:42:13.983542Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=62591 filters=2 travel=0 total=62591 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other" ms=0.8 +2026-04-04T17:42:27.133538Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=62591 parallel=true cells_before_filter=229 cells_after_filter=190 truncated=false bounds=51.5007,-0.1629,51.5329,-0.1068 filters=2 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.1 json_ms=0.2 total_ms=1.3 +2026-04-04T17:42:27.161700Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=24630 parallel=false cells_before_filter=101 cells_after_filter=70 truncated=false bounds=51.5069,-0.1429,51.5231,-0.1171 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.1 json_ms=0.1 total_ms=1.2 +2026-04-04T17:42:27.368301Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=62591 filters=2 travel=0 total=62591 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other" ms=0.8 +2026-04-04T17:42:27.418572Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=24630 filters=1 travel=0 total=18876 filters_raw="Listing status:Historical sale" ms=0.9 +2026-04-04T17:43:36.353481Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=24630 parallel=false cells_before_filter=101 cells_after_filter=57 truncated=false bounds=51.5095,-0.1429,51.5205,-0.1171 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.0 json_ms=0.1 total_ms=1.1 +2026-04-04T17:43:36.480526Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=520 cells_after_filter=420 truncated=false bounds=51.4896,-0.1743,51.5404,-0.0857 filters=5 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.6 json_ms=0.2 total_ms=1.9 +2026-04-04T17:43:36.608010Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=24630 filters=1 travel=0 total=18876 filters_raw="Listing status:Historical sale" ms=0.8 +2026-04-04T17:43:36.717922Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=199182 filters=5 travel=0 total=7158 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" ms=1.1 +2026-04-04T17:43:46.111166Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=520 cells_after_filter=420 truncated=false bounds=51.4896,-0.1743,51.5404,-0.0857 filters=5 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.4 json_ms=0.3 total_ms=1.6 +2026-04-04T17:43:46.375625Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=199182 filters=5 travel=0 total=7158 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" ms=1.1 +2026-04-04T17:44:15.469791Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T17:44:15.473379Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T17:44:15.597503Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=520 cells_after_filter=420 truncated=false bounds=51.4896,-0.1743,51.5404,-0.0857 filters=5 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.4 json_ms=0.2 total_ms=1.6 +2026-04-04T17:44:15.689501Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=24630 parallel=false cells_before_filter=101 cells_after_filter=57 truncated=false bounds=51.5095,-0.1429,51.5205,-0.1171 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.1 json_ms=0.0 total_ms=1.1 +2026-04-04T17:44:15.837137Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=199182 filters=5 travel=0 total=7158 filters_raw="Listing status:Historical sale|For sale|For rent;;Property type:Detached|Semi-Detached|Terraced|Flats/Maisonettes|Other;;Leasehold/Freehold:Freehold|Leasehold;;Asking price:1:inf;;Asking price per sqm:0:49347.58" ms=1.2 +2026-04-04T17:44:15.943048Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=24630 filters=1 travel=0 total=18876 filters_raw="Listing status:Historical sale" ms=0.8 +2026-04-04T17:50:58.654430Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T17:50:58.654914Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T17:50:58.906179Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=24630 parallel=false cells_before_filter=101 cells_after_filter=57 truncated=false bounds=51.5095,-0.1429,51.5205,-0.1171 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=0.9 json_ms=0.0 total_ms=1.0 +2026-04-04T17:50:59.154328Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=24630 filters=1 travel=0 total=18876 filters_raw="Listing status:Historical sale" ms=0.7 +2026-04-04T17:51:01.432180Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=24630 parallel=false cells_before_filter=101 cells_after_filter=68 truncated=false bounds=51.5074,-0.1429,51.5226,-0.1171 filters=1 filters_raw="Listing status:Historical sale" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.1 json_ms=0.1 total_ms=1.2 +2026-04-04T17:51:01.678141Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=24630 filters=1 travel=0 total=18876 filters_raw="Listing status:Historical sale" ms=0.8 +2026-04-04T17:51:44.241484Z INFO property_map_server::routes::travel_destinations: GET /api/travel-destinations mode="transit" results=2752 ms=1.3 +2026-04-04T17:51:44.266755Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T17:51:44.266754Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T17:52:03.103744Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T17:52:03.103746Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T17:52:03.453791Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=267143 parallel=true cells_before_filter=764 cells_after_filter=717 truncated=false bounds=51.4826,-0.1878,51.5498,-0.0708 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.3 json_ms=0.4 total_ms=2.7 +2026-04-04T17:52:03.660920Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=267143 filters=1 travel=0 total=267143 filters_raw="Listing status:Historical sale|For sale|For rent" ms=7.6 +2026-04-04T17:52:08.220466Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T17:52:08.220470Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T17:52:08.480217Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=267143 parallel=true cells_before_filter=764 cells_after_filter=717 truncated=false bounds=51.4826,-0.1878,51.5498,-0.0708 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=2.5 json_ms=0.4 total_ms=2.9 +2026-04-04T17:52:08.735568Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=267143 filters=1 travel=0 total=267143 filters_raw="Listing status:Historical sale|For sale|For rent" ms=7.7 +2026-04-04T17:52:11.068594Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=267143 parallel=true cells_before_filter=764 cells_after_filter=717 truncated=false bounds=51.4826,-0.1878,51.5498,-0.0708 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=2.5 json_ms=0.6 total_ms=3.1 +2026-04-04T17:52:12.668067Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=905315 filters=1 travel=0 total=905315 filters_raw="Listing status:Historical sale|For sale|For rent" ms=26.5 +2026-04-04T17:52:14.001393Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=580 cells_after_filter=457 truncated=false bounds=51.4896,-0.1743,51.5404,-0.0857 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.9 json_ms=0.4 total_ms=2.3 +2026-04-04T17:52:14.335417Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=199182 filters=1 travel=0 total=199182 filters_raw="Listing status:Historical sale|For sale|For rent" ms=5.8 +2026-04-04T17:52:25.878764Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=580 cells_after_filter=457 truncated=false bounds=51.4896,-0.1743,51.5404,-0.0857 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.7 json_ms=0.4 total_ms=2.1 +2026-04-04T17:52:28.714719Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=199182 parallel=true cells_before_filter=580 cells_after_filter=457 truncated=false bounds=51.4896,-0.1743,51.5404,-0.0857 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=2.0 json_ms=0.4 total_ms=2.3 +2026-04-04T17:52:31.103390Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=127926 parallel=true cells_before_filter=456 cells_after_filter=439 truncated=false bounds=51.5036,-0.1614,51.5545,-0.0728 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.5 json_ms=0.3 total_ms=1.9 +2026-04-04T17:52:31.360567Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=127926 filters=1 travel=0 total=127926 filters_raw="Listing status:Historical sale|For sale|For rent" ms=3.8 +2026-04-04T17:59:15.541227Z INFO property_map_server::routes::features: GET /api/features +2026-04-04T17:59:15.541757Z INFO property_map_server::routes::pois: GET /api/poi-categories count=74 groups=11 +2026-04-04T17:59:15.877270Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=127926 parallel=true cells_before_filter=456 cells_after_filter=438 truncated=false bounds=51.5037,-0.1614,51.5545,-0.0728 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=1.5 json_ms=0.2 total_ms=1.7 +2026-04-04T17:59:16.096233Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=127926 filters=1 travel=0 total=127926 filters_raw="Listing status:Historical sale|For sale|For rent" ms=3.7 +2026-04-04T17:59:20.695819Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=127926 parallel=true cells_before_filter=456 cells_after_filter=438 truncated=false bounds=51.5037,-0.1614,51.5545,-0.0728 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.4 json_ms=0.3 total_ms=1.7 +2026-04-04T17:59:22.856529Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=282369 parallel=true cells_before_filter=920 cells_after_filter=895 truncated=false bounds=51.4930,-0.1818,51.5685,-0.0502 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=2.8 json_ms=0.7 total_ms=3.5 +2026-04-04T17:59:23.113378Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=282369 filters=1 travel=0 total=282369 filters_raw="Listing status:Historical sale|For sale|For rent" ms=8.4 +2026-04-04T17:59:33.849596Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=8 rows=2768658 parallel=true cells_before_filter=4398 cells_after_filter=4390 truncated=false bounds=51.3474,-0.4950,51.8318,0.3504 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=18.5 json_ms=3.5 total_ms=22.0 +2026-04-04T17:59:34.622226Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=6 rows=6211194 parallel=true cells_before_filter=1135 cells_after_filter=1135 truncated=false bounds=50.9262,-1.3956,52.5806,1.5025 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=1 travel_entries=0 grid_ms=0.1 agg_ms=35.9 json_ms=0.8 total_ms=36.8 +2026-04-04T17:59:35.085236Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=6211194 filters=1 travel=0 total=6211194 filters_raw="Listing status:Historical sale|For sale|For rent" ms=193.9 +2026-04-04T17:59:36.396469Z INFO property_map_server::routes::hexagon_stats: GET /api/hexagon-stats h3=86194e9b7ffffff resolution=6 total_count=1098 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" ms=0.5 +2026-04-04T17:59:36.773954Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=6 rows=5451197 parallel=true cells_before_filter=874 cells_after_filter=874 truncated=false bounds=50.9262,-0.9915,52.5806,1.0984 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=1 travel_entries=0 grid_ms=0.1 agg_ms=27.2 json_ms=0.7 total_ms=28.0 +2026-04-04T17:59:37.142991Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=5451197 filters=1 travel=0 total=5451197 filters_raw="Listing status:Historical sale|For sale|For rent" ms=156.9 +2026-04-04T17:59:39.269152Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=7 rows=6088960 parallel=true cells_before_filter=6934 cells_after_filter=6934 truncated=false bounds=50.9441,-1.3649,52.5634,1.4717 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=0 travel_entries=0 grid_ms=0.1 agg_ms=29.7 json_ms=3.0 total_ms=32.8 +2026-04-04T17:59:39.655123Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=6088960 filters=1 travel=0 total=6088960 filters_raw="Listing status:Historical sale|For sale|For rent" ms=169.7 +2026-04-04T17:59:41.910149Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=6 rows=10747863 parallel=true cells_before_filter=2393 cells_after_filter=2393 truncated=false bounds=50.4628,-2.6360,53.2683,2.2922 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=0 travel_entries=0 grid_ms=0.2 agg_ms=73.4 json_ms=1.0 total_ms=74.7 +2026-04-04T17:59:42.405417Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=10747863 filters=1 travel=0 total=10747863 filters_raw="Listing status:Historical sale|For sale|For rent" ms=330.0 +2026-04-04T17:59:42.950391Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=6 rows=6563925 parallel=true cells_before_filter=1239 cells_after_filter=1239 truncated=false bounds=50.9045,-1.6593,52.6105,1.3295 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=0 travel_entries=0 grid_ms=0.1 agg_ms=22.3 json_ms=0.5 total_ms=23.0 +2026-04-04T17:59:43.513297Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=7 rows=4732864 parallel=true cells_before_filter=3529 cells_after_filter=3529 truncated=false bounds=51.1470,-1.1192,52.2425,0.7973 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=17.7 json_ms=1.6 total_ms=19.3 +2026-04-04T17:59:43.881130Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=4732864 filters=1 travel=0 total=4732864 filters_raw="Listing status:Historical sale|For sale|For rent" ms=130.5 +2026-04-04T17:59:44.506508Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=7 rows=3704415 parallel=true cells_before_filter=1794 cells_after_filter=1794 truncated=false bounds=51.2788,-0.8243,52.0402,0.5065 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=0 travel_entries=0 grid_ms=0.0 agg_ms=15.6 json_ms=0.8 total_ms=16.5 +2026-04-04T17:59:44.842395Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3704415 filters=1 travel=0 total=3704415 filters_raw="Listing status:Historical sale|For sale|For rent" ms=101.7 +2026-04-04T17:59:47.523197Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=7 rows=3704415 parallel=true cells_before_filter=1794 cells_after_filter=1794 truncated=false bounds=51.2788,-0.8243,52.0402,0.5065 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=16.5 json_ms=1.5 total_ms=18.0 +2026-04-04T17:59:50.166227Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=7 rows=3602016 parallel=true cells_before_filter=1759 cells_after_filter=1759 truncated=false bounds=51.2950,-0.6896,52.0561,0.6412 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=15.9 json_ms=1.2 total_ms=17.2 +2026-04-04T17:59:50.512414Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=3602016 filters=1 travel=0 total=3602016 filters_raw="Listing status:Historical sale|For sale|For rent" ms=102.7 +2026-04-04T17:59:52.795095Z INFO property_map_server::routes::hexagons: GET /api/hexagons resolution=9 rows=48877 parallel=false cells_before_filter=450 cells_after_filter=377 truncated=false bounds=51.6499,-0.0772,51.7102,0.0282 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=1 travel_entries=0 grid_ms=0.0 agg_ms=1.8 json_ms=0.4 total_ms=2.2 +2026-04-04T17:59:53.098866Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=48877 filters=1 travel=0 total=48877 filters_raw="Listing status:Historical sale|For sale|For rent" ms=1.6 +2026-04-04T17:59:54.371508Z INFO property_map_server::routes::postcodes: GET /api/postcodes postcodes_before_filter=307 postcodes_after_filter=144 filtered_out=163 truncated=false bounds=51.672122,-0.041819,51.681388,-0.025617 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=1 travel_entries=0 agg_ms=0.4 json_ms=0.4 total_ms=0.8 +2026-04-04T17:59:54.545340Z INFO property_map_server::routes::postcodes: GET /api/postcodes postcodes_before_filter=307 postcodes_after_filter=147 filtered_out=160 truncated=false bounds=51.672062,-0.041916,51.681467,-0.025470 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=1 travel_entries=0 agg_ms=0.6 json_ms=0.4 total_ms=1.1 +2026-04-04T17:59:54.805768Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=5169 filters=1 travel=0 total=5169 filters_raw="Listing status:Historical sale|For sale|For rent" ms=0.2 +2026-04-04T17:59:55.424629Z INFO property_map_server::routes::postcodes: GET /api/postcodes postcodes_before_filter=112 postcodes_after_filter=25 filtered_out=87 truncated=false bounds=51.675106,-0.037068,51.677521,-0.032845 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=1 travel_entries=0 agg_ms=0.2 json_ms=0.1 total_ms=0.3 +2026-04-04T17:59:55.677260Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=1843 filters=1 travel=0 total=1843 filters_raw="Listing status:Historical sale|For sale|For rent" ms=0.1 +2026-04-04T17:59:56.342774Z INFO property_map_server::routes::postcodes: GET /api/postcodes postcodes_before_filter=112 postcodes_after_filter=12 filtered_out=100 truncated=false bounds=51.675683,-0.036147,51.676772,-0.034244 filters=1 filters_raw="Listing status:Historical sale|For sale|For rent" fields=1 travel_entries=0 agg_ms=0.2 json_ms=0.1 total_ms=0.2 +2026-04-04T17:59:56.590966Z INFO property_map_server::routes::filter_counts: GET /api/filter-counts rows=1843 filters=1 travel=0 total=1843 filters_raw="Listing status:Historical sale|For sale|For rent" ms=0.1 diff --git a/server-rs/src/aggregation.rs b/server-rs/src/aggregation.rs index e269cde..ca4f82f 100644 --- a/server-rs/src/aggregation.rs +++ b/server-rs/src/aggregation.rs @@ -1,6 +1,13 @@ use crate::consts::NAN_U16; use crate::data::QuantRef; +/// Optional per-enum-value distribution tracking for a single feature. +/// Counts how many rows have each enum value (by raw u16 index). +pub struct EnumDist { + pub feat_idx: usize, + pub counts: Box<[u32]>, +} + /// Per-cell accumulator for aggregating features (min/max/sum/count). /// Uses Box<[T]> instead of Vec to avoid storing capacity (saves 8 bytes per field per cell). /// Shared by hexagon and postcode aggregation routes. @@ -10,16 +17,26 @@ pub struct Aggregator { pub maxs: Box<[f32]>, pub sums: Box<[f64]>, pub feat_counts: Box<[u32]>, + /// Optional: per-value counts for a single enum feature (for pie chart visualization). + pub enum_dist: Option, } +/// Configuration for enum distribution tracking, passed to Aggregator::new. +/// (feature_index, number_of_enum_values) +pub type EnumDistConfig = Option<(usize, usize)>; + impl Aggregator { - pub fn new(num_features: usize) -> Self { + pub fn new(num_features: usize, enum_dist_config: EnumDistConfig) -> Self { Aggregator { count: 0, mins: vec![f32::INFINITY; num_features].into_boxed_slice(), maxs: vec![f32::NEG_INFINITY; num_features].into_boxed_slice(), sums: vec![0.0f64; num_features].into_boxed_slice(), feat_counts: vec![0u32; num_features].into_boxed_slice(), + enum_dist: enum_dist_config.map(|(feat_idx, num_values)| EnumDist { + feat_idx, + counts: vec![0u32; num_values].into_boxed_slice(), + }), } } @@ -50,6 +67,17 @@ impl Aggregator { self.feat_counts[feat_index] += 1; } } + // Enum distribution: single branch per row (not per feature). + // Uses raw u16 directly — enum features are stored as u16 indices. + if let Some(ref mut ed) = self.enum_dist { + let raw = row_slice[ed.feat_idx]; + if raw != NAN_U16 { + let idx = raw as usize; + if idx < ed.counts.len() { + ed.counts[idx] += 1; + } + } + } } /// Merge another aggregator's results into this one. @@ -67,6 +95,12 @@ impl Aggregator { self.feat_counts[i] += other.feat_counts[i]; } } + // Merge enum distribution counts + if let (Some(ref mut mine), Some(ref theirs)) = (&mut self.enum_dist, &other.enum_dist) { + for (m, t) in mine.counts.iter_mut().zip(theirs.counts.iter()) { + *m += t; + } + } } /// Add a row, only aggregating the features at the given indices. @@ -95,5 +129,15 @@ impl Aggregator { self.feat_counts[feat_index] += 1; } } + // Enum distribution (same raw u16 approach) + if let Some(ref mut ed) = self.enum_dist { + let raw = feature_data[base + ed.feat_idx]; + if raw != NAN_U16 { + let idx = raw as usize; + if idx < ed.counts.len() { + ed.counts[idx] += 1; + } + } + } } } diff --git a/server-rs/src/parsing/fields.rs b/server-rs/src/parsing/fields.rs index 303b44c..38d7531 100644 --- a/server-rs/src/parsing/fields.rs +++ b/server-rs/src/parsing/fields.rs @@ -31,6 +31,33 @@ pub fn parse_field_indices( Ok(Some(indices)) } +/// Parse an optional `?enum_dist=` query param into (feature_index, num_values) for +/// per-value distribution counting. Returns None if not requested. +/// Returns 400 if the feature name is unknown or not an enum feature. +pub fn parse_enum_dist( + enum_dist: Option<&str>, + name_to_index: &FxHashMap, + enum_values: &FxHashMap>, +) -> Result, (StatusCode, String)> { + let Some(name) = enum_dist else { + return Ok(None); + }; + let name = name.trim(); + if name.is_empty() { + return Ok(None); + } + let &feat_idx = name_to_index + .get(name) + .ok_or_else(|| (StatusCode::BAD_REQUEST, format!("Unknown feature: {name}")))?; + let values = enum_values.get(&feat_idx).ok_or_else(|| { + ( + StatusCode::BAD_REQUEST, + format!("Feature is not an enum: {name}"), + ) + })?; + Ok(Some((feat_idx, values.len()))) +} + /// Parse an optional `?fields=` query param into a HashSet for stats filtering. /// Returns `(fields_specified, field_set)`. pub fn parse_field_set(fields: Option<&str>) -> (bool, HashSet) { diff --git a/server-rs/src/parsing/filters.rs b/server-rs/src/parsing/filters.rs index f87e9f9..7deae32 100644 --- a/server-rs/src/parsing/filters.rs +++ b/server-rs/src/parsing/filters.rs @@ -605,10 +605,14 @@ mod tests { // row 3: price=600, area=300 → fails both let tq = test_quant(2, 2); let feature_data = vec![ - tq.encode(0, 150.0), tq.encode(1, 100.0), // row 0 - tq.encode(0, 600.0), tq.encode(1, 100.0), // row 1 - tq.encode(0, 150.0), tq.encode(1, 300.0), // row 2 - tq.encode(0, 600.0), tq.encode(1, 300.0), // row 3 + tq.encode(0, 150.0), + tq.encode(1, 100.0), // row 0 + tq.encode(0, 600.0), + tq.encode(1, 100.0), // row 1 + tq.encode(0, 150.0), + tq.encode(1, 300.0), // row 2 + tq.encode(0, 600.0), + tq.encode(1, 300.0), // row 3 ]; let filters = vec![ ParsedFilter { @@ -626,10 +630,10 @@ mod tests { let (total, impacts) = count_filter_impacts(&filters, &[], &feature_data, 2, (0..4u32).into_iter()); - assert_eq!(total, 1); // only row 0 passes - assert_eq!(impacts[0], 1); // row 1 fails price only - assert_eq!(impacts[1], 1); // row 2 fails area only - // row 3 fails both → not counted + assert_eq!(total, 1); // only row 0 passes + assert_eq!(impacts[0], 1); // row 1 fails price only + assert_eq!(impacts[1], 1); // row 2 fails area only + // row 3 fails both → not counted } #[test] @@ -640,9 +644,12 @@ mod tests { // row 2: price=600, type=0(A) → fails numeric only let tq = test_quant(2, 1); let feature_data = vec![ - tq.encode(0, 150.0), 0u16, // row 0 - tq.encode(0, 150.0), 2u16, // row 1 - tq.encode(0, 600.0), 0u16, // row 2 + tq.encode(0, 150.0), + 0u16, // row 0 + tq.encode(0, 150.0), + 2u16, // row 1 + tq.encode(0, 600.0), + 0u16, // row 2 ]; let num_filters = vec![ParsedFilter { feat_idx: 0, @@ -662,9 +669,9 @@ mod tests { (0..3u32).into_iter(), ); - assert_eq!(total, 1); // row 0 - assert_eq!(impacts[0], 1); // row 2 fails numeric only → impacts[0] - assert_eq!(impacts[1], 1); // row 1 fails enum only → impacts[1] + assert_eq!(total, 1); // row 0 + assert_eq!(impacts[0], 1); // row 2 fails numeric only → impacts[0] + assert_eq!(impacts[1], 1); // row 1 fails enum only → impacts[1] } #[test] diff --git a/server-rs/src/routes/ai_filters.rs b/server-rs/src/routes/ai_filters.rs index fcf1463..6edd8ff 100644 --- a/server-rs/src/routes/ai_filters.rs +++ b/server-rs/src/routes/ai_filters.rs @@ -946,10 +946,7 @@ pub async fn post_ai_filters( // Auto-inject Listing status filter for historical mode if let Value::Object(ref mut map) = filters { - map.insert( - "Listing status".to_string(), - json!(["Historical sale"]), - ); + map.insert("Listing status".to_string(), json!(["Historical sale"])); } // Count matching properties and refine if too restrictive