idk
This commit is contained in:
parent
a04ac2d857
commit
d43da9708c
47 changed files with 4120 additions and 573 deletions
|
|
@ -154,7 +154,100 @@ function searchResultKey(result: SearchResult): string {
|
|||
return `place:${result.slug}`;
|
||||
}
|
||||
|
||||
export function useLocationSearch(mode?: string) {
|
||||
/** A category-tagged, scored result from the unified `/api/places` ranking. */
|
||||
type UnifiedResultDTO =
|
||||
| { type: 'postcode'; label: string; score: number }
|
||||
| { type: 'address'; address: string; postcode: string; lat: number; lon: number; score: number }
|
||||
| {
|
||||
type: 'place';
|
||||
name: string;
|
||||
slug: string;
|
||||
place_type: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
city?: string;
|
||||
score: number;
|
||||
};
|
||||
|
||||
interface PlacesApiResponse {
|
||||
places: PlaceResult[];
|
||||
postcodes?: string[];
|
||||
addresses?: AddressResult[];
|
||||
/** Preferred: a single relevance-ordered list across all categories. */
|
||||
results?: UnifiedResultDTO[];
|
||||
}
|
||||
|
||||
function isNonNull<T>(value: T | null): value is T {
|
||||
return value !== null;
|
||||
}
|
||||
|
||||
function unifiedToSearchResult(result: UnifiedResultDTO): SearchResult | null {
|
||||
if (result.type === 'postcode') {
|
||||
return { type: 'postcode', label: result.label };
|
||||
}
|
||||
if (result.type === 'address') {
|
||||
return {
|
||||
type: 'address',
|
||||
address: result.address,
|
||||
postcode: result.postcode,
|
||||
lat: result.lat,
|
||||
lon: result.lon,
|
||||
};
|
||||
}
|
||||
if (result.type === 'place') {
|
||||
return {
|
||||
type: 'place',
|
||||
name: result.name,
|
||||
slug: result.slug,
|
||||
place_type: result.place_type,
|
||||
lat: result.lat,
|
||||
lon: result.lon,
|
||||
city: result.city === 'City of London' ? 'London' : result.city,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Legacy ordering for servers that predate the unified `results` list: positional buckets,
|
||||
* re-filtered locally. Retained only as a fallback. */
|
||||
function legacyCombineResults(json: PlacesApiResponse, trimmed: string): SearchResult[] {
|
||||
const placeResults = json.places.map((p) => ({
|
||||
type: 'place' as const,
|
||||
name: p.name,
|
||||
slug: p.slug,
|
||||
place_type: p.place_type,
|
||||
lat: p.lat,
|
||||
lon: p.lon,
|
||||
city: p.city === 'City of London' ? 'London' : p.city,
|
||||
}));
|
||||
const outcodeResults = placeResults.filter((result) => result.place_type === 'outcode');
|
||||
const otherPlaceResults = placeResults.filter((result) => result.place_type !== 'outcode');
|
||||
const postcodeResults: SearchResult[] = (json.postcodes ?? []).map((postcode) => ({
|
||||
type: 'postcode' as const,
|
||||
label: postcode,
|
||||
}));
|
||||
const addressResults: SearchResult[] = (json.addresses ?? []).map((address) => ({
|
||||
type: 'address' as const,
|
||||
address: address.address,
|
||||
postcode: address.postcode,
|
||||
lat: address.lat,
|
||||
lon: address.lon,
|
||||
}));
|
||||
const containsHouseNumber = /\d/.test(trimmed);
|
||||
return filterResultsForQuery(
|
||||
containsHouseNumber
|
||||
? [...outcodeResults, ...postcodeResults, ...addressResults, ...otherPlaceResults]
|
||||
: [...outcodeResults, ...postcodeResults, ...otherPlaceResults, ...addressResults],
|
||||
trimmed
|
||||
);
|
||||
}
|
||||
|
||||
export type ViewportCenter = { lat: number; lng: number };
|
||||
|
||||
export function useLocationSearch(
|
||||
mode?: string,
|
||||
getViewportCenter?: () => ViewportCenter | null
|
||||
) {
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState<SearchResult[]>([]);
|
||||
const [recentSearches, setRecentSearches] = useState<SearchResult[]>(readRecentSearches);
|
||||
|
|
@ -165,6 +258,9 @@ export function useLocationSearch(mode?: string) {
|
|||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const latestQueryRef = useRef('');
|
||||
const lastResultsRef = useRef<SearchResult[]>([]);
|
||||
// Held in a ref so a non-memoized callback from the parent doesn't churn handleInputChange.
|
||||
const getViewportCenterRef = useRef(getViewportCenter);
|
||||
getViewportCenterRef.current = getViewportCenter;
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(value: string) => {
|
||||
|
|
@ -212,6 +308,11 @@ export function useLocationSearch(mode?: string) {
|
|||
try {
|
||||
const params = new URLSearchParams({ q: trimmed });
|
||||
if (mode) params.set('mode', mode);
|
||||
const center = getViewportCenterRef.current?.();
|
||||
if (center) {
|
||||
params.set('lat', String(center.lat));
|
||||
params.set('lng', String(center.lng));
|
||||
}
|
||||
const res = await fetch(
|
||||
`/api/places?${params}`,
|
||||
authHeaders({ signal: controller.signal })
|
||||
|
|
@ -223,47 +324,19 @@ export function useLocationSearch(mode?: string) {
|
|||
}
|
||||
return;
|
||||
}
|
||||
const json: {
|
||||
places: PlaceResult[];
|
||||
postcodes?: string[];
|
||||
addresses?: AddressResult[];
|
||||
} = await res.json();
|
||||
const placeResults = json.places.map((p) => ({
|
||||
type: 'place' as const,
|
||||
name: p.name,
|
||||
slug: p.slug,
|
||||
place_type: p.place_type,
|
||||
lat: p.lat,
|
||||
lon: p.lon,
|
||||
city: p.city === 'City of London' ? 'London' : p.city,
|
||||
}));
|
||||
const outcodeResults = placeResults.filter((result) => result.place_type === 'outcode');
|
||||
const otherPlaceResults = placeResults.filter(
|
||||
(result) => result.place_type !== 'outcode'
|
||||
);
|
||||
const postcodeResults: SearchResult[] = (json.postcodes ?? []).map((postcode) => ({
|
||||
type: 'postcode' as const,
|
||||
label: postcode,
|
||||
}));
|
||||
const addressResults: SearchResult[] = (json.addresses ?? []).map((address) => ({
|
||||
type: 'address' as const,
|
||||
address: address.address,
|
||||
postcode: address.postcode,
|
||||
lat: address.lat,
|
||||
lon: address.lon,
|
||||
}));
|
||||
const containsHouseNumber = /\d/.test(trimmed);
|
||||
const json: PlacesApiResponse = await res.json();
|
||||
const combinedResults = (
|
||||
containsHouseNumber
|
||||
? [...outcodeResults, ...postcodeResults, ...addressResults, ...otherPlaceResults]
|
||||
: [...outcodeResults, ...postcodeResults, ...otherPlaceResults, ...addressResults]
|
||||
Array.isArray(json.results)
|
||||
? json.results.map(unifiedToSearchResult).filter(isNonNull)
|
||||
: legacyCombineResults(json, trimmed)
|
||||
).slice(0, 20);
|
||||
if (controller.signal.aborted || latestQueryRef.current.trim() !== trimmed) {
|
||||
return;
|
||||
}
|
||||
lastResultsRef.current = combinedResults;
|
||||
const matchingResults = filterResultsForQuery(combinedResults, trimmed);
|
||||
setResults(matchingResults);
|
||||
// Trust the server's unified ranking — re-filtering here previously dropped valid
|
||||
// alias and partial-postcode matches. The optimistic pre-fetch path still filters.
|
||||
setResults(combinedResults);
|
||||
setOpen(true);
|
||||
} catch (err) {
|
||||
logNonAbortError('places search', err);
|
||||
|
|
|
|||
|
|
@ -178,7 +178,11 @@ describe('resolveTransitVariant', () => {
|
|||
|
||||
describe('parseServerMode', () => {
|
||||
it('round-trips the four toggle-reachable variants', () => {
|
||||
expect(parseServerMode('transit')).toEqual({ mode: 'transit', noChange: false, noBuses: false });
|
||||
expect(parseServerMode('transit')).toEqual({
|
||||
mode: 'transit',
|
||||
noChange: false,
|
||||
noBuses: false,
|
||||
});
|
||||
expect(parseServerMode('transit-no-bus')).toEqual({
|
||||
mode: 'transit',
|
||||
noChange: false,
|
||||
|
|
@ -198,8 +202,16 @@ describe('parseServerMode', () => {
|
|||
|
||||
it('parses non-transit base modes', () => {
|
||||
expect(parseServerMode('car')).toEqual({ mode: 'car', noChange: false, noBuses: false });
|
||||
expect(parseServerMode('bicycle')).toEqual({ mode: 'bicycle', noChange: false, noBuses: false });
|
||||
expect(parseServerMode('walking')).toEqual({ mode: 'walking', noChange: false, noBuses: false });
|
||||
expect(parseServerMode('bicycle')).toEqual({
|
||||
mode: 'bicycle',
|
||||
noChange: false,
|
||||
noBuses: false,
|
||||
});
|
||||
expect(parseServerMode('walking')).toEqual({
|
||||
mode: 'walking',
|
||||
noChange: false,
|
||||
noBuses: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null for variants the UI cannot represent (no silent broadening)', () => {
|
||||
|
|
|
|||
|
|
@ -138,7 +138,15 @@ export function useTravelTime(initial?: TravelTimeInitial) {
|
|||
const handleAddEntry = useCallback((mode: TransportMode) => {
|
||||
setEntries((prev) => [
|
||||
...prev,
|
||||
{ mode, slug: '', label: '', timeRange: null, useBest: false, noChange: false, noBuses: false },
|
||||
{
|
||||
mode,
|
||||
slug: '',
|
||||
label: '',
|
||||
timeRange: null,
|
||||
useBest: false,
|
||||
noChange: false,
|
||||
noBuses: false,
|
||||
},
|
||||
]);
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue