From a04ac2d857fe0df879e7023667129f9a709247e5 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 2 Jun 2026 08:21:47 +0100 Subject: [PATCH] . --- Makefile.data | 4 +- .../components/map/LocationSearch.test.tsx | 81 ++++++++++++++++++- frontend/src/components/map/Map.tsx | 5 ++ frontend/src/components/map/MobileDrawer.tsx | 5 +- .../map/map-page/useExportController.ts | 4 +- frontend/src/components/ui/ExportMenu.tsx | 12 +-- frontend/src/components/ui/FeatureLabel.tsx | 4 +- frontend/src/hooks/useHexagonSelection.ts | 7 +- frontend/src/i18n/locales/de.ts | 24 ++---- frontend/src/i18n/locales/fr.ts | 9 +-- frontend/src/i18n/locales/hi.ts | 24 +++--- frontend/src/i18n/locales/hu.ts | 3 +- frontend/src/i18n/locales/zh.ts | 6 +- pipeline/download/satellite_highres.py | 2 - .../transform/postcode_boundaries/README.md | 4 +- server-rs/src/routes/export.rs | 12 +-- 16 files changed, 132 insertions(+), 74 deletions(-) diff --git a/Makefile.data b/Makefile.data index 5319cea..ecc065e 100644 --- a/Makefile.data +++ b/Makefile.data @@ -327,8 +327,8 @@ $(GREENSPACE): $(PBF) $(OS_GREENSPACE): uv run python -m pipeline.download.os_greenspace --output $@ -$(PLACES): $(PBF) $(ENGLAND_BOUNDARY) $(NAPTAN) $(OFS_REGISTER) $(ARCGIS) - uv run python -m pipeline.download.places --output $@ --pbf $(PBF) --boundary $(ENGLAND_BOUNDARY) --naptan $(NAPTAN) --university-register $(OFS_REGISTER) --postcodes $(ARCGIS) +$(PLACES): $(PBF) $(ENGLAND_BOUNDARY) $(NAPTAN) $(OFS_REGISTER) $(ARCGIS) $(POIS_RAW) + uv run python -m pipeline.download.places --output $@ --pbf $(PBF) --boundary $(ENGLAND_BOUNDARY) --naptan $(NAPTAN) --university-register $(OFS_REGISTER) --postcodes $(ARCGIS) --pois $(POIS_RAW) --include-streets $(MEDIAN_AGE): diff --git a/frontend/src/components/map/LocationSearch.test.tsx b/frontend/src/components/map/LocationSearch.test.tsx index 0c45f17..7b5774a 100644 --- a/frontend/src/components/map/LocationSearch.test.tsx +++ b/frontend/src/components/map/LocationSearch.test.tsx @@ -291,7 +291,69 @@ describe('LocationSearch', () => { }); }); - it('shows an empty state for invalid place queries', async () => { + it('preserves the server unified ordering and sends the viewport centre', async () => { + const fetchMock = vi.fn((input: string | URL | Request) => { + const url = new URL(String(input), 'http://localhost'); + if (url.pathname === '/api/places') { + return Promise.resolve( + jsonResponse({ + places: [], + postcodes: [], + addresses: [], + // Intentionally out of "bucket" order: an address outranks a place. The hook must + // render them in this server order rather than re-bucketing or re-filtering. + results: [ + { + type: 'address', + address: '1 High Street', + postcode: 'CR0 1AA', + lat: 51.37, + lon: -0.1, + score: 930, + }, + { + type: 'place', + name: 'Croydon', + slug: 'croydon', + place_type: 'town', + lat: 51.37, + lon: -0.1, + score: 880, + }, + ], + }) + ); + } + return Promise.resolve(new Response(null, { status: 404 })); + }); + vi.stubGlobal('fetch', fetchMock); + + render( + ({ lat: 51.5, lng: -0.12 })} + /> + ); + + fireEvent.change(screen.getByRole('textbox'), { target: { value: 'high street' } }); + + // The address (first in the server list) renders before the place, unfiltered. + const firstResult = await screen.findByText('1 High Street'); + const place = screen.getByText('Croydon'); + expect( + firstResult.compareDocumentPosition(place) & Node.DOCUMENT_POSITION_FOLLOWING + ).toBeTruthy(); + + const placesCall = fetchMock.mock.calls.find(([input]) => + String(input).includes('/api/places') + ); + const calledUrl = new URL(String(placesCall?.[0]), 'http://localhost'); + expect(calledUrl.searchParams.get('lat')).toBe('51.5'); + expect(calledUrl.searchParams.get('lng')).toBe('-0.12'); + }); + + it('shows an empty state for invalid place queries (legacy server, no results key)', async () => { vi.stubGlobal( 'fetch', vi.fn(() => @@ -314,6 +376,23 @@ describe('LocationSearch', () => { }); }); + it('shows the empty state when the new server returns an empty results array', async () => { + vi.stubGlobal( + 'fetch', + vi.fn(() => + Promise.resolve(jsonResponse({ places: [], postcodes: [], addresses: [], results: [] })) + ) + ); + + render(); + + fireEvent.change(screen.getByRole('textbox'), { target: { value: 'zzzznowhere' } }); + + await waitFor(() => { + expect(screen.getByText('No matching places or postcodes')).toBeTruthy(); + }); + }); + it('keeps only the three most recent local searches', async () => { vi.stubGlobal( 'fetch', diff --git a/frontend/src/components/map/Map.tsx b/frontend/src/components/map/Map.tsx index f1b81dc..423cf1b 100644 --- a/frontend/src/components/map/Map.tsx +++ b/frontend/src/components/map/Map.tsx @@ -937,6 +937,10 @@ export default memo(function Map({ dimensions.width < DESKTOP_TOP_CARDS_ROW_MIN_MAP_WIDTH; const showLocationSearch = !hideLocationSearch && !hideDesktopTopCardsForWidth; const showLegend = !hideLegend && !hideDesktopTopCardsForWidth; + const getViewportCenter = useCallback(() => { + const center = mapRef.current?.getCenter(); + return center ? { lat: center.lat, lng: center.lng } : null; + }, []); const desktopTopCardsLayoutClass = stackDesktopTopCards ? 'flex-col items-start' : 'items-start justify-between'; @@ -1099,6 +1103,7 @@ export default memo(function Map({ onLocationSearched={onLocationSearched} onCurrentLocationFound={onCurrentLocationFound} onMouseEnter={handleMouseLeave} + getViewportCenter={getViewportCenter} className={DESKTOP_TOP_CARD_CLASS} inputClassName={DESKTOP_LOCATION_SEARCH_INPUT_CLASS} /> diff --git a/frontend/src/components/map/MobileDrawer.tsx b/frontend/src/components/map/MobileDrawer.tsx index 58a6718..39bdf6c 100644 --- a/frontend/src/components/map/MobileDrawer.tsx +++ b/frontend/src/components/map/MobileDrawer.tsx @@ -103,7 +103,10 @@ export default function MobileDrawer({ }, []); return ( -
+