updates on the bookings page and updated ai and more thngs
This commit is contained in:
352
src/lib/geocodingService.js
Normal file
352
src/lib/geocodingService.js
Normal file
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* Enterprise Geocoding & Address Autocomplete Service
|
||||
*
|
||||
* Supports multi-provider hierarchy:
|
||||
* 1. Ola Maps Places Autocomplete (if VITE_OLA_MAPS_API_KEY is configured)
|
||||
* 2. Google Places API (if VITE_GOOGLE_MAPS_API_KEY is configured)
|
||||
* 3. Photon (Komoot OSM - high-speed, typo-tolerant, sub-100ms autocomplete)
|
||||
* 4. Nominatim (OpenStreetMap standard geocoder with country lock)
|
||||
*/
|
||||
|
||||
const getEnvVar = (key) => {
|
||||
try {
|
||||
if (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env[key]) {
|
||||
return import.meta.env[key];
|
||||
}
|
||||
} catch {}
|
||||
try {
|
||||
if (typeof process !== 'undefined' && process.env && process.env[key]) {
|
||||
return process.env[key];
|
||||
}
|
||||
} catch {}
|
||||
return '';
|
||||
};
|
||||
|
||||
const OLA_MAPS_KEY = getEnvVar('VITE_OLA_MAPS_API_KEY');
|
||||
|
||||
const PHOTON_API_URL = 'https://photon.komoot.io/api';
|
||||
const PHOTON_REVERSE_URL = 'https://photon.komoot.io/reverse';
|
||||
const NOMINATIM_SEARCH_URL = 'https://nominatim.openstreetmap.org/search';
|
||||
const NOMINATIM_REVERSE_URL = 'https://nominatim.openstreetmap.org/reverse';
|
||||
|
||||
/**
|
||||
* Standardize place result into unified format for Doormile order forms
|
||||
*/
|
||||
export const standardizePlace = ({
|
||||
formatted_address = '',
|
||||
name = '',
|
||||
suburb = '',
|
||||
city = '',
|
||||
state = '',
|
||||
postcode = '',
|
||||
latitude = null,
|
||||
longitude = null,
|
||||
provider = 'unknown',
|
||||
raw = null
|
||||
}) => {
|
||||
const latNum = latitude != null ? Number(latitude) : null;
|
||||
const lngNum = longitude != null ? Number(longitude) : null;
|
||||
|
||||
return {
|
||||
formatted_address: formatted_address || name,
|
||||
name: name || formatted_address.split(',')[0] || '',
|
||||
suburb: suburb || '',
|
||||
city: city || '',
|
||||
state: state || '',
|
||||
postcode: postcode || '',
|
||||
latitude: Number.isFinite(latNum) ? latNum : null,
|
||||
longitude: Number.isFinite(lngNum) ? lngNum : null,
|
||||
provider,
|
||||
raw,
|
||||
// Google Places / Legacy compatibility interface
|
||||
geometry: {
|
||||
location: {
|
||||
lat: () => (Number.isFinite(latNum) ? latNum : 0),
|
||||
lng: () => (Number.isFinite(lngNum) ? lngNum : 0)
|
||||
}
|
||||
},
|
||||
address_components: [
|
||||
...(suburb ? [{ long_name: suburb, short_name: suburb, types: ['sublocality_level_1', 'sublocality'] }] : []),
|
||||
...(city ? [{ long_name: city, short_name: city, types: ['locality'] }] : []),
|
||||
...(state ? [{ long_name: state, short_name: state, types: ['administrative_area_level_1'] }] : []),
|
||||
...(postcode ? [{ long_name: postcode, short_name: postcode, types: ['postal_code'] }] : [])
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 1. Photon (Komoot OSM) Search - Fast, typo-tolerant, ideal for real-time keystroke suggestions
|
||||
*/
|
||||
async function searchPhoton(query, { bias, limit = 6 } = {}) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
q: query,
|
||||
limit: String(limit),
|
||||
lang: 'en'
|
||||
});
|
||||
|
||||
if (bias?.lat && bias?.lng) {
|
||||
params.set('lat', String(bias.lat));
|
||||
params.set('lon', String(bias.lng));
|
||||
}
|
||||
|
||||
const res = await fetch(`${PHOTON_API_URL}?${params.toString()}`, {
|
||||
headers: { Accept: 'application/json' }
|
||||
});
|
||||
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
if (!data?.features || !Array.isArray(data.features)) return [];
|
||||
|
||||
return data.features.map((item) => {
|
||||
const p = item.properties || {};
|
||||
const [lon, lat] = item.geometry?.coordinates || [null, null];
|
||||
|
||||
const parts = [
|
||||
p.name,
|
||||
p.street ? `${p.housenumber ? p.housenumber + ' ' : ''}${p.street}` : '',
|
||||
p.district || p.suburb || p.neighbourhood || p.locality,
|
||||
p.city,
|
||||
p.state,
|
||||
p.postcode,
|
||||
p.country
|
||||
].filter(Boolean);
|
||||
|
||||
// Remove consecutive duplicates in address string
|
||||
const uniqueParts = parts.filter((part, idx, arr) => arr.indexOf(part) === idx);
|
||||
const formatted = uniqueParts.join(', ');
|
||||
|
||||
return standardizePlace({
|
||||
formatted_address: formatted || p.name || '',
|
||||
name: p.name || formatted.split(',')[0],
|
||||
suburb: p.district || p.suburb || p.neighbourhood || p.locality || '',
|
||||
city: p.city || p.county || '',
|
||||
state: p.state || '',
|
||||
postcode: p.postcode || '',
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
provider: 'photon',
|
||||
raw: item
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('[Geocoding] Photon search failed:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. Nominatim Search - Fallback with countrycodes=in & addressdetails
|
||||
*/
|
||||
async function searchNominatim(query, { bias, country = 'in', limit = 5 } = {}) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
q: query,
|
||||
format: 'json',
|
||||
addressdetails: '1',
|
||||
limit: String(limit)
|
||||
});
|
||||
|
||||
if (country) {
|
||||
params.set('countrycodes', country.toLowerCase());
|
||||
}
|
||||
|
||||
if (bias?.lat && bias?.lng) {
|
||||
const d = 0.5; // ~55km bounding box
|
||||
params.set('viewbox', `${bias.lng - d},${bias.lat + d},${bias.lng + d},${bias.lat - d}`);
|
||||
}
|
||||
|
||||
const res = await fetch(`${NOMINATIM_SEARCH_URL}?${params.toString()}`, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Accept-Language': 'en-GB,en;q=0.9',
|
||||
'User-Agent': 'DoormileConsole/1.0'
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) return [];
|
||||
const results = await res.json();
|
||||
if (!Array.isArray(results)) return [];
|
||||
|
||||
return results.map((r) => {
|
||||
const addr = r.address || {};
|
||||
const suburb = addr.suburb || addr.neighbourhood || addr.quarter || addr.residential || '';
|
||||
const city = addr.city || addr.town || addr.village || addr.city_district || addr.county || '';
|
||||
const state = addr.state || '';
|
||||
const postcode = addr.postcode || '';
|
||||
|
||||
return standardizePlace({
|
||||
formatted_address: r.display_name || '',
|
||||
name: r.display_name?.split(',')[0] || '',
|
||||
suburb,
|
||||
city,
|
||||
state,
|
||||
postcode,
|
||||
latitude: r.lat,
|
||||
longitude: r.lon,
|
||||
provider: 'nominatim',
|
||||
raw: r
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('[Geocoding] Nominatim search failed:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 3. Ola Maps Places Autocomplete (if key configured)
|
||||
*/
|
||||
async function searchOlaMaps(query, { bias, limit = 5 } = {}) {
|
||||
if (!OLA_MAPS_KEY) return [];
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
input: query,
|
||||
api_key: OLA_MAPS_KEY
|
||||
});
|
||||
if (bias?.lat && bias?.lng) {
|
||||
params.set('location', `${bias.lat},${bias.lng}`);
|
||||
params.set('radius', '50000');
|
||||
}
|
||||
|
||||
const res = await fetch(`https://api.olamaps.io/places/v1/autocomplete?${params.toString()}`);
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
if (!data?.predictions || !Array.isArray(data.predictions)) return [];
|
||||
|
||||
return data.predictions.slice(0, limit).map((p) => {
|
||||
const lat = p.geometry?.location?.lat;
|
||||
const lng = p.geometry?.location?.lng;
|
||||
return standardizePlace({
|
||||
formatted_address: p.description || '',
|
||||
name: p.structured_formatting?.main_text || p.description?.split(',')[0] || '',
|
||||
suburb: p.structured_formatting?.secondary_text?.split(',')[0] || '',
|
||||
city: '',
|
||||
state: '',
|
||||
postcode: '',
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
provider: 'olamaps',
|
||||
raw: p
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('[Geocoding] Ola Maps autocomplete failed:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary Address Autocomplete Dispatcher
|
||||
* Queries fastest/best provider first, then falls back seamlessly.
|
||||
*/
|
||||
export async function getAddressSuggestions(query, options = {}) {
|
||||
const cleanQuery = (query || '').trim();
|
||||
if (!cleanQuery || cleanQuery.length < 2) return [];
|
||||
|
||||
// 1. Try Ola Maps if API key is present
|
||||
if (OLA_MAPS_KEY) {
|
||||
const olaResults = await searchOlaMaps(cleanQuery, options);
|
||||
if (olaResults.length > 0) return olaResults;
|
||||
}
|
||||
|
||||
// 2. High-speed Photon OSM autocomplete (sub-100ms, typo tolerant)
|
||||
const photonResults = await searchPhoton(cleanQuery, options);
|
||||
if (photonResults.length > 0) return photonResults;
|
||||
|
||||
// 3. Fallback to scoped Nominatim
|
||||
const nominatimResults = await searchNominatim(cleanQuery, options);
|
||||
return nominatimResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse Geocode: Convert Coordinates to structured address
|
||||
*/
|
||||
export async function reverseGeocode(lat, lng) {
|
||||
if (lat == null || lng == null) return null;
|
||||
|
||||
// 1. Try Photon reverse first
|
||||
try {
|
||||
const res = await fetch(`${PHOTON_REVERSE_URL}?lat=${lat}&lon=${lng}`, {
|
||||
headers: { Accept: 'application/json' }
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data?.features?.length > 0) {
|
||||
const item = data.features[0];
|
||||
const p = item.properties || {};
|
||||
const parts = [
|
||||
p.name,
|
||||
p.street ? `${p.housenumber ? p.housenumber + ' ' : ''}${p.street}` : '',
|
||||
p.district || p.suburb || p.neighbourhood || p.locality,
|
||||
p.city,
|
||||
p.state,
|
||||
p.postcode,
|
||||
p.country
|
||||
].filter(Boolean);
|
||||
const formatted = parts.filter((part, idx, arr) => arr.indexOf(part) === idx).join(', ');
|
||||
|
||||
return standardizePlace({
|
||||
formatted_address: formatted,
|
||||
name: p.name || formatted.split(',')[0],
|
||||
suburb: p.district || p.suburb || p.neighbourhood || p.locality || '',
|
||||
city: p.city || p.county || '',
|
||||
state: p.state || '',
|
||||
postcode: p.postcode || '',
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
provider: 'photon_reverse'
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Geocoding] Photon reverse failed, trying Nominatim:', e);
|
||||
}
|
||||
|
||||
// 2. Fallback to Nominatim reverse
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${NOMINATIM_REVERSE_URL}?lat=${lat}&lon=${lng}&format=json&zoom=18&addressdetails=1`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Accept-Language': 'en-GB,en;q=0.9',
|
||||
'User-Agent': 'DoormileConsole/1.0'
|
||||
}
|
||||
}
|
||||
);
|
||||
if (res.ok) {
|
||||
const r = await res.json();
|
||||
const addr = r.address || {};
|
||||
return standardizePlace({
|
||||
formatted_address: r.display_name || '',
|
||||
name: r.display_name?.split(',')[0] || '',
|
||||
suburb: addr.suburb || addr.neighbourhood || addr.quarter || addr.residential || '',
|
||||
city: addr.city || addr.town || addr.village || addr.city_district || addr.county || '',
|
||||
state: addr.state || '',
|
||||
postcode: addr.postcode || '',
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
provider: 'nominatim_reverse'
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Geocoding] Reverse geocode failed completely:', err);
|
||||
}
|
||||
|
||||
return standardizePlace({
|
||||
formatted_address: `Location (${Number(lat).toFixed(5)}, ${Number(lng).toFixed(5)})`,
|
||||
name: 'Selected Pin',
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
provider: 'coordinates_only'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct forward geocode for single address string (used by bulk uploads / fallback)
|
||||
*/
|
||||
export async function geocodeAddress(address, options = {}) {
|
||||
const suggestions = await getAddressSuggestions(address, { ...options, limit: 1 });
|
||||
return suggestions.length > 0 ? suggestions[0] : null;
|
||||
}
|
||||
Reference in New Issue
Block a user