Lots of improvements

This commit is contained in:
Andras Schmelczer 2026-02-22 21:09:07 +00:00
parent 205302dbb8
commit eb02b5832b
39 changed files with 699 additions and 271 deletions

View file

@ -4,6 +4,8 @@ export interface HexagonLocation {
lat: number;
lon: number;
resolution: number;
postcode?: string;
isPostcode?: boolean;
}
const PROPERTY_TYPE_MAP: Record<
@ -32,10 +34,10 @@ export const H3_RADIUS_MILES: Record<number, number> = {
6: 3,
7: 1,
8: 0.5,
9: 0.25,
10: 0.25,
11: 0.25,
12: 0.25,
9: 1,
10: 1,
11: 1,
12: 1,
};
const RIGHTMOVE_RADII = [0.25, 0.5, 1, 3, 5, 10, 15, 20, 30, 40];
@ -46,13 +48,21 @@ function nearestRadius(target: number, allowed: number[]): number {
return allowed.reduce((best, r) => (Math.abs(r - target) < Math.abs(best - target) ? r : best));
}
export function buildPropertySearchUrls(
location: HexagonLocation,
filters: FeatureFilters
): { rightmove: string; onthemarket: string; zoopla: string } {
const { lat, lon, resolution } = location;
const radiusMiles = H3_RADIUS_MILES[resolution] ?? 1;
const coordStr = `${lat.toFixed(5)},${lon.toFixed(5)}`;
interface SearchUrlOptions {
location: HexagonLocation;
filters: FeatureFilters;
rightmoveLocationId?: string;
}
export function buildPropertySearchUrls({
location,
filters,
rightmoveLocationId,
}: SearchUrlOptions): { rightmove: string | null; onthemarket: string; zoopla: string } | null {
const { postcode, resolution, isPostcode } = location;
if (!postcode) return null;
const radiusMiles = isPostcode ? 0.25 : (H3_RADIUS_MILES[resolution] ?? 1);
const priceFilter = filters['Last known price'];
const minPrice =
@ -66,43 +76,51 @@ export function buildPropertySearchUrls(
? (propertyTypes as string[])
: [];
const rmParams = new URLSearchParams();
rmParams.set('searchLocation', coordStr);
rmParams.set('channel', 'BUY');
rmParams.set('radius', String(nearestRadius(radiusMiles, RIGHTMOVE_RADII)));
if (minPrice !== undefined) rmParams.set('minPrice', String(Math.round(minPrice)));
if (maxPrice !== undefined) rmParams.set('maxPrice', String(Math.round(maxPrice)));
if (selectedTypes.length > 0) {
const rmTypes = [
...new Set(
selectedTypes.flatMap((t) => {
const mapped = PROPERTY_TYPE_MAP[t]?.rightmove;
return mapped ? mapped.split(',') : [];
})
),
];
if (rmTypes.length > 0) rmParams.set('propertyTypes', rmTypes.join(','));
// Rightmove — requires locationIdentifier from typeahead API
let rightmove: string | null = null;
if (rightmoveLocationId) {
const rmParams = new URLSearchParams();
rmParams.set('searchLocation', postcode);
rmParams.set('useLocationIdentifier', 'true');
rmParams.set('locationIdentifier', rightmoveLocationId);
rmParams.set('radius', String(nearestRadius(radiusMiles, RIGHTMOVE_RADII)));
if (minPrice !== undefined) rmParams.set('minPrice', String(Math.round(minPrice)));
if (maxPrice !== undefined) rmParams.set('maxPrice', String(Math.round(maxPrice)));
if (selectedTypes.length > 0) {
const rmTypes = [
...new Set(
selectedTypes.flatMap((t) => {
const mapped = PROPERTY_TYPE_MAP[t]?.rightmove;
return mapped ? mapped.split(',') : [];
})
),
];
if (rmTypes.length > 0) rmParams.set('propertyTypes', rmTypes.join(','));
}
rmParams.set('_includeSSTC', 'on');
rightmove = `https://www.rightmove.co.uk/property-for-sale/find.html?${rmParams.toString()}`;
}
const rightmove = `https://www.rightmove.co.uk/property-for-sale/find.html?${rmParams.toString()}`;
let otmType = 'property';
if (selectedTypes.length > 0) {
const otmTypes = [
...new Set(selectedTypes.map((t) => PROPERTY_TYPE_MAP[t]?.onthemarket).filter(Boolean)),
];
if (otmTypes.length === 1 && otmTypes[0] !== 'property') otmType = otmTypes[0]!;
}
// OnTheMarket — postcode slug in URL path (e.g. "SW1A 1AA" → "sw1a-1aa")
const otmSlug = postcode.toLowerCase().replace(/\s+/g, '-');
const otmParams = new URLSearchParams();
otmParams.set('radius', String(nearestRadius(radiusMiles, OTM_RADII)));
if (minPrice !== undefined) otmParams.set('min-price', String(Math.round(minPrice)));
if (maxPrice !== undefined) otmParams.set('max-price', String(Math.round(maxPrice)));
otmParams.set('search-site', 'geo');
otmParams.set('geo-lat', String(lat));
otmParams.set('geo-lng', String(lon));
const onthemarket = `https://www.onthemarket.com/for-sale/${otmType}/?${otmParams.toString()}`;
if (selectedTypes.length > 0) {
const otmTypes = [
...new Set(selectedTypes.map((t) => PROPERTY_TYPE_MAP[t]?.onthemarket).filter(Boolean)),
];
for (const ot of otmTypes) {
otmParams.append('prop-types', ot!);
}
}
otmParams.set('view', 'map-list');
const onthemarket = `https://www.onthemarket.com/for-sale/property/${otmSlug}/?${otmParams.toString()}`;
// Zoopla
const zParams = new URLSearchParams();
zParams.set('q', coordStr);
zParams.set('q', postcode);
zParams.set('search_source', 'for-sale');
zParams.set('radius', String(nearestRadius(radiusMiles, ZOOPLA_RADII)));
if (minPrice !== undefined) zParams.set('price_min', String(Math.round(minPrice)));
@ -115,7 +133,6 @@ export function buildPropertySearchUrls(
zParams.append('property_sub_type', zt!);
}
}
zParams.set('geo_autocomplete_identifier', `geo_${lat}_${lon}`);
const zoopla = `https://www.zoopla.co.uk/for-sale/property/?${zParams.toString()}`;
return { rightmove, onthemarket, zoopla };