This commit is contained in:
Andras Schmelczer 2026-05-13 08:00:12 +01:00
parent 63713c3a2b
commit bd6b511f16
17 changed files with 544 additions and 377 deletions

View file

@ -54,7 +54,10 @@ describe('useHexagonSelection', () => {
requests.push(`${url.pathname}${url.search}`);
if (url.pathname === '/api/postcode-stats') {
return Promise.resolve(jsonResponse(stats(url.searchParams.has('filters') ? 0 : 4)));
const emptyPostcode = url.searchParams.get('postcode') === 'EMPTY 1AA';
return Promise.resolve(
jsonResponse(stats(url.searchParams.has('filters') || emptyPostcode ? 0 : 4))
);
}
if (url.pathname === '/api/hexagon-stats') {
@ -91,12 +94,15 @@ describe('useHexagonSelection', () => {
id: 'SW1A 1AA',
type: 'postcode',
resolution: 9,
lockedResolution: true,
});
});
expect(result.current.selectedPostcodeGeometry).toBe(postcodeGeometry);
expect(result.current.areaStats?.count).toBe(0);
expect(result.current.unfilteredAreaCount).toBe(4);
await waitFor(() => {
expect(result.current.areaStats?.count).toBe(0);
expect(result.current.unfilteredAreaCount).toBe(4);
});
expect(requests.some((url) => url.startsWith('/api/hexagon-stats'))).toBe(false);
});
@ -124,6 +130,7 @@ describe('useHexagonSelection', () => {
id: 'SW1A 1AA',
type: 'postcode',
resolution: 9,
lockedResolution: true,
});
});
@ -131,4 +138,65 @@ describe('useHexagonSelection', () => {
expect(result.current.unfilteredAreaCount).toBeNull();
expect(requests.some((url) => url.startsWith('/api/hexagon-stats'))).toBe(false);
});
it('keeps an empty postcode search selected instead of widening to hexagons', async () => {
const { result } = renderHook(() =>
useHexagonSelection({
filters: {},
features,
hexagonData: [],
resolution: 9,
usePostcodeView: true,
travelTimeEntries: [],
})
);
act(() => {
result.current.handleLocationSearch('EMPTY 1AA', postcodeGeometry, 51.505, -0.115);
});
await waitFor(() => {
expect(result.current.areaStats?.count).toBe(0);
});
expect(result.current.selectedHexagon).toEqual({
id: 'EMPTY 1AA',
type: 'postcode',
resolution: 9,
lockedResolution: true,
});
expect(result.current.selectedPostcodeGeometry).toBe(postcodeGeometry);
expect(requests.some((url) => url.startsWith('/api/hexagon-stats'))).toBe(false);
});
it('does not convert a searched postcode back to a hexagon while the map reaches postcode zoom', async () => {
const { result } = renderHook(() =>
useHexagonSelection({
filters: {},
features,
hexagonData: [],
resolution: 9,
usePostcodeView: false,
travelTimeEntries: [],
})
);
act(() => {
result.current.handleLocationSearch('SW1A 1AA', postcodeGeometry, 51.505, -0.115);
});
await waitFor(() => {
expect(result.current.areaStats?.count).toBe(4);
});
expect(result.current.selectedHexagon).toEqual({
id: 'SW1A 1AA',
type: 'postcode',
resolution: 9,
lockedResolution: true,
});
expect(result.current.selectedPostcodeGeometry).toBe(postcodeGeometry);
expect(requests.some((url) => url.startsWith('/api/postcode/'))).toBe(false);
expect(requests.some((url) => url.startsWith('/api/hexagon-stats'))).toBe(false);
});
});

View file

@ -426,7 +426,7 @@ export function useHexagonSelection({
selection.type === 'hexagon' &&
!selection.lockedResolution &&
areaStats?.central_postcode != null) ||
(!usePostcodeView && selection.type === 'postcode') ||
(!usePostcodeView && selection.type === 'postcode' && !selection.lockedResolution) ||
(!usePostcodeView &&
selection.type === 'hexagon' &&
!selection.lockedResolution &&
@ -628,103 +628,38 @@ export function useHexagonSelection({
(
postcode: string,
geometry: PostcodeGeometry,
lat?: number,
lng?: number,
_lat?: number,
_lng?: number,
openProperties = false,
focusAddress?: string
) => {
const requestId = invalidateAreaRequests();
invalidatePropertyRequests();
const selection = {
id: postcode,
type: 'postcode' as const,
resolution,
lockedResolution: true,
};
trackEvent(openProperties ? 'Address Search' : 'Postcode Search');
setSelectedHexagon(selection);
setSelectedPostcodeGeometry(geometry);
setProperties([]);
setPropertiesTotal(0);
setPropertiesOffset(0);
setAreaStats(null);
setUnfilteredAreaCount(null);
setRightPaneTab(openProperties ? 'properties' : 'area');
setLoadingAreaStats(true);
// First try the postcode; if it only has no matches because of active filters,
// keep the searched postcode selected instead of widening to nearby hexagons.
fetchPostcodeStats(postcode, undefined, areaStatsUseFilters)
.then(async (stats) => {
.then((stats) => {
if (!isCurrentAreaRequest(requestId)) return;
if (stats.count > 0) {
const selection = { id: postcode, type: 'postcode' as const, resolution };
setSelectedHexagon(selection);
setSelectedPostcodeGeometry(geometry);
setAreaStats(stats);
refreshUnfilteredAreaCount(selection, stats.count, areaStatsUseFilters);
if (openProperties) {
fetchPostcodeProperties(postcode, 0, focusAddress);
}
return;
setAreaStats(stats);
refreshUnfilteredAreaCount(selection, stats.count, areaStatsUseFilters);
if (openProperties && stats.count > 0) {
fetchPostcodeProperties(postcode, 0, focusAddress);
}
if (areaStatsUseFilters && hasStatsFilters) {
const unfilteredStats = await fetchPostcodeStats(postcode, undefined, false);
if (!isCurrentAreaRequest(requestId)) return;
if (unfilteredStats.count > 0) {
const selection = { id: postcode, type: 'postcode' as const, resolution };
setSelectedHexagon(selection);
setSelectedPostcodeGeometry(geometry);
setAreaStats(stats);
setUnfilteredAreaCount(unfilteredStats.count);
setRightPaneTab(openProperties ? 'properties' : 'area');
return;
}
}
// No properties in this postcode — fall back to hexagons
if (lat == null || lng == null) {
// No coordinates available, show empty postcode anyway
const selection = { id: postcode, type: 'postcode' as const, resolution };
setSelectedHexagon(selection);
setSelectedPostcodeGeometry(geometry);
setAreaStats(stats);
refreshUnfilteredAreaCount(selection, stats.count, areaStatsUseFilters);
setRightPaneTab('area');
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,
undefined,
undefined,
areaStatsUseFilters
);
if (!isCurrentAreaRequest(requestId)) return;
if (hexStats.count > 1) {
const selection = { id: h3, type: 'hexagon' as const, resolution: res };
setSelectedHexagon(selection);
setSelectedPostcodeGeometry(null);
setAreaStats(hexStats);
refreshUnfilteredAreaCount(selection, hexStats.count, areaStatsUseFilters);
setRightPaneTab('area');
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,
undefined,
undefined,
areaStatsUseFilters
);
if (!isCurrentAreaRequest(requestId)) return;
const selection = { id: h3, type: 'hexagon' as const, resolution: 9 };
setSelectedHexagon(selection);
setSelectedPostcodeGeometry(null);
setAreaStats(fallbackStats);
refreshUnfilteredAreaCount(selection, fallbackStats.count, areaStatsUseFilters);
setRightPaneTab('area');
})
.catch((error) => logNonAbortError('Failed to fetch postcode stats', error))
.finally(() => {
@ -734,9 +669,7 @@ export function useHexagonSelection({
[
resolution,
areaStatsUseFilters,
hasStatsFilters,
fetchPostcodeStats,
fetchHexagonStats,
fetchPostcodeProperties,
invalidateAreaRequests,
invalidatePropertyRequests,