import type { FeatureFilters } from '../types'; export interface HexagonLocation { lat: number; lon: number; resolution: number; postcode?: string; isPostcode?: boolean; } const PROPERTY_TYPE_MAP: Record< string, { rightmove: string; onthemarket: string; zoopla: string } > = { House: { rightmove: 'detached,semi-detached,terraced', onthemarket: 'property', zoopla: '' }, Detached: { rightmove: 'detached', onthemarket: 'detached', zoopla: 'detached' }, 'Semi-Detached': { rightmove: 'semi-detached', onthemarket: 'semi-detached', zoopla: 'semi_detached', }, 'Mid-Terrace': { rightmove: 'terraced', onthemarket: 'terraced', zoopla: 'terraced' }, 'End-Terrace': { rightmove: 'terraced', onthemarket: 'terraced', zoopla: 'terraced' }, 'Enclosed Mid-Terrace': { rightmove: 'terraced', onthemarket: 'terraced', zoopla: 'terraced' }, 'Enclosed End-Terrace': { rightmove: 'terraced', onthemarket: 'terraced', zoopla: 'terraced' }, 'Flats/Maisonettes': { rightmove: 'flat', onthemarket: 'flats', zoopla: 'flat' }, Bungalow: { rightmove: 'bungalow', onthemarket: 'bungalow', zoopla: 'bungalow' }, 'Park home': { rightmove: 'park-home', onthemarket: 'property', zoopla: '' }, }; export const H3_RADIUS_MILES: Record = { 4: 15, 5: 6, 6: 3, 7: 1, 8: 0.5, 9: 1, 10: 1, 11: 1, 12: 1, }; const RIGHTMOVE_RADII = [0.25, 0.5, 1, 3, 5, 10, 15, 20, 30, 40]; const OTM_RADII = [0.25, 0.5, 1, 3, 5, 10, 15, 20, 30, 40]; const ZOOPLA_RADII = [0.25, 0.5, 1, 3, 5, 10, 15, 20, 25, 30]; function nearestRadius(target: number, allowed: number[]): number { return allowed.reduce((best, r) => (Math.abs(r - target) < Math.abs(best - target) ? r : best)); } 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 = Array.isArray(priceFilter) && typeof priceFilter[0] === 'number' ? priceFilter[0] : undefined; const maxPrice = Array.isArray(priceFilter) && typeof priceFilter[1] === 'number' ? priceFilter[1] : undefined; const propertyTypes = filters['Property type']; const selectedTypes = Array.isArray(propertyTypes) && typeof propertyTypes[0] === 'string' ? (propertyTypes as string[]) : []; // 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()}`; } // 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))); 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', 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))); if (maxPrice !== undefined) zParams.set('price_max', String(Math.round(maxPrice))); if (selectedTypes.length > 0) { const zTypes = [ ...new Set(selectedTypes.map((t) => PROPERTY_TYPE_MAP[t]?.zoopla).filter(Boolean)), ]; for (const zt of zTypes) { zParams.append('property_sub_type', zt!); } } const zoopla = `https://www.zoopla.co.uk/for-sale/property/?${zParams.toString()}`; return { rightmove, onthemarket, zoopla }; }