updates on the autocomplete in the create order page
This commit is contained in:
@@ -1,50 +1,58 @@
|
||||
/**
|
||||
* Calculates distance (in km) between origin and destination.
|
||||
* Uses OSRM as primary driving distance source, and falls back to Haversine with a 1.3 multiplier.
|
||||
*
|
||||
* @param {Object} origin - { latitude, longitude }
|
||||
* @param {Object} destination - { latitude, longitude }
|
||||
* @returns {Promise<number>} - Round distance in KM
|
||||
* Driving Distance, Duration & Route Polyline Calculator for DoorMile
|
||||
* Uses OSRM as primary routing engine with GeoJSON polyline extraction,
|
||||
* and falls back seamlessly to Haversine with a 1.3 road winding multiplier.
|
||||
*/
|
||||
// Duration from the most recent OSRM route, in minutes, or null when the
|
||||
// Haversine fallback was used (a straight-line estimate has no honest
|
||||
// duration — better to show nothing than a made-up ETA).
|
||||
|
||||
let lastRouteDurationMin = null;
|
||||
|
||||
export const getLastRouteDurationMin = () => lastRouteDurationMin;
|
||||
|
||||
export const calculateDrivingDistance = async (origin, destination) => {
|
||||
const lat1 = origin?.latitude;
|
||||
const lon1 = origin?.longitude;
|
||||
const lat2 = destination?.latitude;
|
||||
const lon2 = destination?.longitude;
|
||||
/**
|
||||
* Calculates complete driving route with distance, duration, and road polyline
|
||||
* @param {Object} origin - { latitude, longitude }
|
||||
* @param {Object} destination - { latitude, longitude }
|
||||
* @returns {Promise<{ distance: number, minutes: number, polyline: Array<[number, number]>, resolved: boolean }>}
|
||||
*/
|
||||
export const calculateDrivingRoute = async (origin, destination) => {
|
||||
const lat1 = Number(origin?.latitude);
|
||||
const lon1 = Number(origin?.longitude);
|
||||
const lat2 = Number(destination?.latitude);
|
||||
const lon2 = Number(destination?.longitude);
|
||||
|
||||
if (lat1 == null || lon1 == null || lat2 == null || lon2 == null) {
|
||||
if (!Number.isFinite(lat1) || !Number.isFinite(lon1) || !Number.isFinite(lat2) || !Number.isFinite(lon2)) {
|
||||
throw new Error("Invalid coordinates");
|
||||
}
|
||||
|
||||
// 1. Try OSRM API (either self-hosted or public demo server for fallback)
|
||||
const osrmBaseUrl = import.meta.env.VITE_OSRM_URL || "https://router.project-osrm.org";
|
||||
// 1. Try OSRM API with full GeoJSON geometry for live polyline map preview
|
||||
const osrmBaseUrl =
|
||||
(typeof import.meta !== 'undefined' && import.meta?.env?.VITE_OSRM_URL) ||
|
||||
'https://router.project-osrm.org';
|
||||
try {
|
||||
const url = `${osrmBaseUrl}/route/v1/driving/${lon1},${lat1};${lon2},${lat2}?overview=false`;
|
||||
const url = `${osrmBaseUrl}/route/v1/driving/${lon1},${lat1};${lon2},${lat2}?overview=full&geometries=geojson`;
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.routes && data.routes.length > 0) {
|
||||
const distanceInMeters = data.routes[0].distance;
|
||||
const distanceInKm = distanceInMeters / 1000;
|
||||
// OSRM already returns the driving duration alongside the distance;
|
||||
// it was being discarded. Cached here so callers that want an ETA get
|
||||
// a REAL routed figure instead of dividing km by a guessed speed.
|
||||
lastRouteDurationMin = Number.isFinite(data.routes[0].duration) ? Math.round(data.routes[0].duration / 60) : null;
|
||||
return Math.round(distanceInKm);
|
||||
const route = data.routes[0];
|
||||
const distanceKm = Math.round(route.distance / 1000);
|
||||
const durationMin = Number.isFinite(route.duration) ? Math.round(route.duration / 60) : null;
|
||||
// OSRM geojson coordinates are [lng, lat] -> convert to Leaflet [lat, lng]
|
||||
const polyline = (route.geometry?.coordinates || []).map(([lng, lat]) => [lat, lng]);
|
||||
lastRouteDurationMin = durationMin;
|
||||
return {
|
||||
distance: Math.max(0, distanceKm),
|
||||
minutes: durationMin,
|
||||
polyline: polyline.length > 0 ? polyline : [[lat1, lon1], [lat2, lon2]],
|
||||
resolved: true
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("OSRM API failed, falling back to Haversine math:", error);
|
||||
}
|
||||
|
||||
// 2. Fallback: Haversine Formula with 1.3x multiplier
|
||||
// 2. Fallback: Haversine Formula with 1.3x road factor
|
||||
const toRad = (val) => (val * Math.PI) / 180;
|
||||
const R = 6371; // Earth radius in km
|
||||
const dLat = toRad(lat2 - lat1);
|
||||
@@ -54,25 +62,36 @@ export const calculateDrivingDistance = async (origin, destination) => {
|
||||
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
const aerialDistance = R * c;
|
||||
const aerialDistance = Math.round(R * c * 1.3);
|
||||
|
||||
// No route was fetched, so there is no honest duration to report.
|
||||
lastRouteDurationMin = null;
|
||||
return Math.round(aerialDistance * 1.3);
|
||||
return {
|
||||
distance: aerialDistance,
|
||||
minutes: null,
|
||||
polyline: [[lat1, lon1], [lat2, lon2]],
|
||||
resolved: true
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates distance (in km) between origin and destination
|
||||
*/
|
||||
export const calculateDrivingDistance = async (origin, destination) => {
|
||||
const res = await calculateDrivingRoute(origin, destination);
|
||||
return res.distance;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates total charge based on distance and pricing tier
|
||||
*
|
||||
* @param {number} distanceKm
|
||||
* @param {number} basePrice
|
||||
* @param {number} pricePerKm
|
||||
* @param {number} minKm
|
||||
* @returns {number}
|
||||
*/
|
||||
export const calculateTotalCharge = (distanceKm, basePrice, pricePerKm, minKm) => {
|
||||
if (distanceKm < minKm) {
|
||||
return basePrice;
|
||||
const dist = Number(distanceKm) || 0;
|
||||
const base = Number(basePrice) || 40;
|
||||
const perKm = Number(pricePerKm) || 10;
|
||||
const min = Number(minKm) || 2;
|
||||
|
||||
if (dist <= min) {
|
||||
return base;
|
||||
}
|
||||
return (distanceKm - minKm) * pricePerKm + basePrice;
|
||||
return (dist - min) * perKm + base;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
/**
|
||||
* Enterprise Geocoding & Address Autocomplete Service
|
||||
* Enterprise Dynamic Geocoding & Address Autocomplete Service for DoorMile
|
||||
*
|
||||
* 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)
|
||||
* 100% Dynamic, Functional Geocoding Pipeline:
|
||||
* 1. Door / Flat / Plot Prefix Parser
|
||||
* 2. Google Maps URL & Raw Coordinates Paste Parser
|
||||
* 3. 6-Digit Indian Pincode Auto-Resolver
|
||||
* 4. India-Scoped High-Speed Photon (OSM) with Bounding Box
|
||||
* 5. India-Locked Nominatim Search (countrycodes=in)
|
||||
* 6. Recent Addresses Local Storage Manager
|
||||
* 7. Automatic Landmark Extractor
|
||||
*/
|
||||
|
||||
const getEnvVar = (key) => {
|
||||
@@ -29,8 +32,92 @@ 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';
|
||||
|
||||
// Geographic boundary of India (minLon, minLat, maxLon, maxLat)
|
||||
const INDIA_BOUNDS = {
|
||||
minLat: 6.5,
|
||||
maxLat: 37.5,
|
||||
minLng: 68.0,
|
||||
maxLng: 97.5
|
||||
};
|
||||
const INDIA_BBOX_PHOTON = '68.0,6.5,97.5,37.5';
|
||||
|
||||
const RECENT_ADDRESSES_STORAGE_KEY = 'doormile_recent_addresses';
|
||||
|
||||
/**
|
||||
* Standardize place result into unified format for Doormile order forms
|
||||
* Strict boundary validator ensuring suggestions stay inside India
|
||||
*/
|
||||
export const isWithinIndia = (lat, lon) => {
|
||||
const nLat = Number(lat);
|
||||
const nLon = Number(lon);
|
||||
if (!Number.isFinite(nLat) || !Number.isFinite(nLon)) return false;
|
||||
return (
|
||||
nLat >= INDIA_BOUNDS.minLat &&
|
||||
nLat <= INDIA_BOUNDS.maxLat &&
|
||||
nLon >= INDIA_BOUNDS.minLng &&
|
||||
nLon <= INDIA_BOUNDS.maxLng
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts door / flat / plot / house number prefix
|
||||
*/
|
||||
export const extractDoorPrefix = (text) => {
|
||||
if (!text) return { doorNo: '', cleanQuery: '' };
|
||||
const trimmed = text.trim();
|
||||
const match = trimmed.match(
|
||||
/^((?:no\.?|d\.?no\.?|flat|door|plot|#|house|shop)\s*[\w\d\-\/]+)(?:,\s*|\s+)(.+)$/i
|
||||
);
|
||||
if (match) {
|
||||
return { doorNo: match[1].trim(), cleanQuery: match[2].trim() };
|
||||
}
|
||||
return { doorNo: '', cleanQuery: trimmed };
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts landmark cues from an address string (e.g. "Opposite Inorbit Mall", "Near Metro Pillar 12")
|
||||
*/
|
||||
export const extractLandmark = (address) => {
|
||||
if (!address) return '';
|
||||
const match = address.match(/(?:near|opp\.?|opposite|behind|beside|next to|adj\.?|adjacent to)\s+([^,]+)/i);
|
||||
return match ? match[0].trim() : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Detects and parses pasted Google Maps links or raw coordinates
|
||||
* e.g. "https://maps.google.com/?q=17.4483,78.3915" or "17.4483, 78.3915"
|
||||
*/
|
||||
export const parseRawCoordinatesOrUrl = (text) => {
|
||||
if (!text) return null;
|
||||
const clean = text.trim();
|
||||
|
||||
// 1. Raw Coordinates regex: "17.4483, 78.3915" or "17.4483 78.3915"
|
||||
const coordMatch = clean.match(/^(-?\d{1,2}\.\d+)[,\s]+(-?\d{1,3}\.\d+)$/);
|
||||
if (coordMatch) {
|
||||
const lat = Number(coordMatch[1]);
|
||||
const lng = Number(coordMatch[2]);
|
||||
if (isWithinIndia(lat, lng)) return { lat, lng };
|
||||
}
|
||||
|
||||
// 2. Google Maps URL parser: contains q=lat,lng or @lat,lng
|
||||
const urlCoordMatch = clean.match(/(?:q=|@|destination=)(-?\d{1,2}\.\d+)[,\s]+(-?\d{1,3}\.\d+)/);
|
||||
if (urlCoordMatch) {
|
||||
const lat = Number(urlCoordMatch[1]);
|
||||
const lng = Number(urlCoordMatch[2]);
|
||||
if (isWithinIndia(lat, lng)) return { lat, lng };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const REQUEST_TIMEOUT_MS = 3000;
|
||||
const fetchWithTimeout = (url, options = {}) => {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
||||
return fetch(url, { ...options, signal: controller.signal }).finally(() => clearTimeout(timer));
|
||||
};
|
||||
|
||||
/**
|
||||
* Standardize place result into unified format
|
||||
*/
|
||||
export const standardizePlace = ({
|
||||
formatted_address = '',
|
||||
@@ -58,7 +145,6 @@ export const standardizePlace = ({
|
||||
longitude: Number.isFinite(lngNum) ? lngNum : null,
|
||||
provider,
|
||||
raw,
|
||||
// Google Places / Legacy compatibility interface
|
||||
geometry: {
|
||||
location: {
|
||||
lat: () => (Number.isFinite(latNum) ? latNum : 0),
|
||||
@@ -75,14 +161,46 @@ export const standardizePlace = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* 1. Photon (Komoot OSM) Search - Fast, typo-tolerant, ideal for real-time keystroke suggestions
|
||||
* Recent Addresses Local Storage Management
|
||||
*/
|
||||
async function searchPhoton(query, { bias, limit = 6 } = {}) {
|
||||
export const getRecentAddresses = (limit = 5) => {
|
||||
try {
|
||||
const saved = localStorage.getItem(RECENT_ADDRESSES_STORAGE_KEY);
|
||||
if (!saved) return [];
|
||||
const list = JSON.parse(saved);
|
||||
return Array.isArray(list) ? list.slice(0, limit) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const saveRecentAddress = (place) => {
|
||||
if (!place || !place.formatted_address || !place.latitude || !place.longitude) return;
|
||||
try {
|
||||
const list = getRecentAddresses(10);
|
||||
const filtered = list.filter(
|
||||
(item) =>
|
||||
item.formatted_address !== place.formatted_address &&
|
||||
Math.hypot(item.latitude - place.latitude, item.longitude - place.longitude) > 0.002
|
||||
);
|
||||
filtered.unshift(place);
|
||||
localStorage.setItem(RECENT_ADDRESSES_STORAGE_KEY, JSON.stringify(filtered.slice(0, 8)));
|
||||
} catch {}
|
||||
};
|
||||
|
||||
/**
|
||||
* 1. Photon (Komoot OSM) Dynamic Search
|
||||
*/
|
||||
async function searchPhoton(query, { bias, limit = 10 } = {}) {
|
||||
try {
|
||||
const { doorNo, cleanQuery } = extractDoorPrefix(query);
|
||||
const searchQuery = cleanQuery || query;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
q: query,
|
||||
q: searchQuery,
|
||||
limit: String(limit),
|
||||
lang: 'en'
|
||||
lang: 'en',
|
||||
bbox: INDIA_BBOX_PHOTON
|
||||
});
|
||||
|
||||
if (bias?.lat && bias?.lng) {
|
||||
@@ -90,7 +208,7 @@ async function searchPhoton(query, { bias, limit = 6 } = {}) {
|
||||
params.set('lon', String(bias.lng));
|
||||
}
|
||||
|
||||
const res = await fetch(`${PHOTON_API_URL}?${params.toString()}`, {
|
||||
const res = await fetchWithTimeout(`${PHOTON_API_URL}?${params.toString()}`, {
|
||||
headers: { Accept: 'application/json' }
|
||||
});
|
||||
|
||||
@@ -98,69 +216,83 @@ async function searchPhoton(query, { bias, limit = 6 } = {}) {
|
||||
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];
|
||||
return data.features
|
||||
.filter((item) => {
|
||||
const [lon, lat] = item.geometry?.coordinates || [null, null];
|
||||
return isWithinIndia(lat, lon);
|
||||
})
|
||||
.map((item) => {
|
||||
const p = item.properties || {};
|
||||
const [lon, lat] = item.geometry?.coordinates || [null, null];
|
||||
const finalHouse = doorNo || p.housenumber || '';
|
||||
|
||||
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 primaryName = finalHouse
|
||||
? `${finalHouse}, ${p.name || ''}`.trim().replace(/^,\s*/, '')
|
||||
: p.name || '';
|
||||
|
||||
// Remove consecutive duplicates in address string
|
||||
const uniqueParts = parts.filter((part, idx, arr) => arr.indexOf(part) === idx);
|
||||
const formatted = uniqueParts.join(', ');
|
||||
const parts = [
|
||||
primaryName,
|
||||
p.street ? `${!doorNo && p.housenumber ? p.housenumber + ' ' : ''}${p.street}` : '',
|
||||
p.district || p.suburb || p.neighbourhood || p.locality,
|
||||
p.city || p.county || '',
|
||||
p.state || '',
|
||||
p.postcode || '',
|
||||
p.country || 'India'
|
||||
].filter(Boolean);
|
||||
|
||||
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
|
||||
const uniqueParts = parts.filter((part, idx, arr) => arr.indexOf(part) === idx);
|
||||
const formatted = uniqueParts.join(', ');
|
||||
|
||||
return standardizePlace({
|
||||
formatted_address: formatted || primaryName,
|
||||
name: primaryName || 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
|
||||
* 2. Nominatim Dynamic Search
|
||||
*/
|
||||
async function searchNominatim(query, { bias, country = 'in', limit = 5 } = {}) {
|
||||
async function searchNominatim(query, { bias, limit = 8 } = {}) {
|
||||
try {
|
||||
const { doorNo, cleanQuery } = extractDoorPrefix(query);
|
||||
const searchQuery = cleanQuery || query;
|
||||
const isPincode = /^[1-9][0-9]{5}$/.test(searchQuery.trim());
|
||||
|
||||
const params = new URLSearchParams({
|
||||
q: query,
|
||||
format: 'json',
|
||||
addressdetails: '1',
|
||||
limit: String(limit)
|
||||
limit: String(limit),
|
||||
countrycodes: 'in'
|
||||
});
|
||||
|
||||
if (country) {
|
||||
params.set('countrycodes', country.toLowerCase());
|
||||
if (isPincode) {
|
||||
params.set('postalcode', searchQuery.trim());
|
||||
} else {
|
||||
params.set('q', searchQuery);
|
||||
}
|
||||
|
||||
if (bias?.lat && bias?.lng) {
|
||||
const d = 0.5; // ~55km bounding box
|
||||
const d = 0.8;
|
||||
params.set('viewbox', `${bias.lng - d},${bias.lat + d},${bias.lng + d},${bias.lat - d}`);
|
||||
}
|
||||
|
||||
const res = await fetch(`${NOMINATIM_SEARCH_URL}?${params.toString()}`, {
|
||||
const res = await fetchWithTimeout(`${NOMINATIM_SEARCH_URL}?${params.toString()}`, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Accept-Language': 'en-GB,en;q=0.9',
|
||||
'User-Agent': 'DoormileConsole/1.0'
|
||||
'User-Agent': 'DoormileConsole/2.0'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -168,34 +300,44 @@ async function searchNominatim(query, { bias, country = 'in', limit = 5 } = {})
|
||||
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 results
|
||||
.filter((r) => isWithinIndia(r.lat, r.lon))
|
||||
.map((r) => {
|
||||
const addr = r.address || {};
|
||||
const suburb = addr.suburb || addr.neighbourhood || addr.quarter || addr.residential || addr.district || '';
|
||||
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
|
||||
let displayName = r.display_name || '';
|
||||
if (doorNo && !displayName.toLowerCase().startsWith(doorNo.toLowerCase())) {
|
||||
displayName = `${doorNo}, ${displayName}`;
|
||||
}
|
||||
|
||||
const primaryName = doorNo
|
||||
? `${doorNo}, ${r.name || displayName.split(',')[0]}`
|
||||
: r.name || displayName.split(',')[0];
|
||||
|
||||
return standardizePlace({
|
||||
formatted_address: displayName,
|
||||
name: primaryName,
|
||||
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)
|
||||
* 3. Ola Maps Places Autocomplete
|
||||
*/
|
||||
async function searchOlaMaps(query, { bias, limit = 5 } = {}) {
|
||||
if (!OLA_MAPS_KEY) return [];
|
||||
@@ -209,65 +351,126 @@ async function searchOlaMaps(query, { bias, limit = 5 } = {}) {
|
||||
params.set('radius', '50000');
|
||||
}
|
||||
|
||||
const res = await fetch(`https://api.olamaps.io/places/v1/autocomplete?${params.toString()}`);
|
||||
const res = await fetchWithTimeout(`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
|
||||
return data.predictions
|
||||
.filter((p) => {
|
||||
const lat = p.geometry?.location?.lat;
|
||||
const lng = p.geometry?.location?.lng;
|
||||
return isWithinIndia(lat, lng);
|
||||
})
|
||||
.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.
|
||||
* Deduplicates results and ranks by active city or geographic proximity
|
||||
*/
|
||||
export async function getAddressSuggestions(query, options = {}) {
|
||||
const cleanQuery = (query || '').trim();
|
||||
if (!cleanQuery || cleanQuery.length < 2) return [];
|
||||
function deduplicateAndRank(places, bias, limit = 8) {
|
||||
const seen = new Set();
|
||||
const unique = [];
|
||||
|
||||
// 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;
|
||||
for (const p of places) {
|
||||
if (!p || !p.formatted_address) continue;
|
||||
const coordKey = p.latitude && p.longitude
|
||||
? `${Number(p.latitude).toFixed(3)},${Number(p.longitude).toFixed(3)}`
|
||||
: p.formatted_address.toLowerCase().slice(0, 35);
|
||||
|
||||
if (seen.has(coordKey)) continue;
|
||||
seen.add(coordKey);
|
||||
unique.push(p);
|
||||
}
|
||||
|
||||
// 2. High-speed Photon OSM autocomplete (sub-100ms, typo tolerant)
|
||||
const photonResults = await searchPhoton(cleanQuery, options);
|
||||
if (photonResults.length > 0) return photonResults;
|
||||
// Sort by bias city or proximity
|
||||
if (bias?.city) {
|
||||
const biasCity = bias.city.toLowerCase();
|
||||
unique.sort((a, b) => {
|
||||
const aMatches = (a.city || a.formatted_address || '').toLowerCase().includes(biasCity) ? 0 : 1;
|
||||
const bMatches = (b.city || b.formatted_address || '').toLowerCase().includes(biasCity) ? 0 : 1;
|
||||
return aMatches - bMatches;
|
||||
});
|
||||
} else if (bias?.lat && bias?.lng) {
|
||||
const bLat = Number(bias.lat);
|
||||
const bLng = Number(bias.lng);
|
||||
unique.sort((a, b) => {
|
||||
const distA = a.latitude && a.longitude ? Math.hypot(a.latitude - bLat, a.longitude - bLng) : 999;
|
||||
const distB = b.latitude && b.longitude ? Math.hypot(b.latitude - bLat, b.longitude - bLng) : 999;
|
||||
return distA - distB;
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Fallback to scoped Nominatim
|
||||
const nominatimResults = await searchNominatim(cleanQuery, options);
|
||||
return nominatimResults;
|
||||
return unique.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse Geocode: Convert Coordinates to structured address
|
||||
* Primary Address Autocomplete Dispatcher
|
||||
*/
|
||||
export async function reverseGeocode(lat, lng) {
|
||||
if (lat == null || lng == null) return null;
|
||||
export async function getAddressSuggestions(query, options = {}) {
|
||||
const cleanQuery = (query || '').trim();
|
||||
if (!cleanQuery) return [];
|
||||
|
||||
// 1. Try Photon reverse first
|
||||
// Check if user pasted coordinates or Google Maps link directly
|
||||
const parsedCoords = parseRawCoordinatesOrUrl(cleanQuery);
|
||||
if (parsedCoords) {
|
||||
const directPlace = await reverseGeocode(parsedCoords.lat, parsedCoords.lng);
|
||||
if (directPlace) return [directPlace];
|
||||
}
|
||||
|
||||
if (cleanQuery.length < 2) return [];
|
||||
|
||||
// Parallel Execution
|
||||
const promises = [];
|
||||
if (OLA_MAPS_KEY) {
|
||||
promises.push(searchOlaMaps(cleanQuery, options));
|
||||
}
|
||||
promises.push(searchPhoton(cleanQuery, options));
|
||||
promises.push(searchNominatim(cleanQuery, options));
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
const places = [];
|
||||
|
||||
for (const res of results) {
|
||||
if (res.status === 'fulfilled' && Array.isArray(res.value)) {
|
||||
places.push(...res.value);
|
||||
}
|
||||
}
|
||||
|
||||
return deduplicateAndRank(places, options.bias, options.limit || 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic Reverse Geocoding
|
||||
*/
|
||||
export async function reverseGeocode(lat, lon) {
|
||||
if (lat == null || lon == null) return null;
|
||||
const nLat = Number(lat);
|
||||
const nLon = Number(lon);
|
||||
if (!isWithinIndia(nLat, nLon)) return null;
|
||||
|
||||
// 1. Try Photon reverse
|
||||
try {
|
||||
const res = await fetch(`${PHOTON_REVERSE_URL}?lat=${lat}&lon=${lng}`, {
|
||||
const res = await fetchWithTimeout(`${PHOTON_REVERSE_URL}?lat=${nLat}&lon=${nLon}`, {
|
||||
headers: { Accept: 'application/json' }
|
||||
});
|
||||
if (res.ok) {
|
||||
@@ -279,11 +482,12 @@ export async function reverseGeocode(lat, lng) {
|
||||
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
|
||||
p.city || p.county || '',
|
||||
p.state || '',
|
||||
p.postcode || '',
|
||||
p.country || 'India'
|
||||
].filter(Boolean);
|
||||
|
||||
const formatted = parts.filter((part, idx, arr) => arr.indexOf(part) === idx).join(', ');
|
||||
|
||||
return standardizePlace({
|
||||
@@ -293,25 +497,23 @@ export async function reverseGeocode(lat, lng) {
|
||||
city: p.city || p.county || '',
|
||||
state: p.state || '',
|
||||
postcode: p.postcode || '',
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
latitude: nLat,
|
||||
longitude: nLon,
|
||||
provider: 'photon_reverse'
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Geocoding] Photon reverse failed, trying Nominatim:', e);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// 2. Fallback to Nominatim reverse
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${NOMINATIM_REVERSE_URL}?lat=${lat}&lon=${lng}&format=json&zoom=18&addressdetails=1`,
|
||||
const res = await fetchWithTimeout(
|
||||
`${NOMINATIM_REVERSE_URL}?lat=${nLat}&lon=${nLon}&format=json&zoom=18&addressdetails=1`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Accept-Language': 'en-GB,en;q=0.9',
|
||||
'User-Agent': 'DoormileConsole/1.0'
|
||||
'User-Agent': 'DoormileConsole/2.0'
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -321,30 +523,28 @@ export async function reverseGeocode(lat, lng) {
|
||||
return standardizePlace({
|
||||
formatted_address: r.display_name || '',
|
||||
name: r.display_name?.split(',')[0] || '',
|
||||
suburb: addr.suburb || addr.neighbourhood || addr.quarter || addr.residential || '',
|
||||
suburb: addr.suburb || addr.neighbourhood || addr.quarter || addr.residential || addr.district || '',
|
||||
city: addr.city || addr.town || addr.village || addr.city_district || addr.county || '',
|
||||
state: addr.state || '',
|
||||
postcode: addr.postcode || '',
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
latitude: nLat,
|
||||
longitude: nLon,
|
||||
provider: 'nominatim_reverse'
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Geocoding] Reverse geocode failed completely:', err);
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
return standardizePlace({
|
||||
formatted_address: `Location (${Number(lat).toFixed(5)}, ${Number(lng).toFixed(5)})`,
|
||||
formatted_address: `Location (${nLat.toFixed(5)}, ${nLon.toFixed(5)})`,
|
||||
name: 'Selected Pin',
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
latitude: nLat,
|
||||
longitude: nLon,
|
||||
provider: 'coordinates_only'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct forward geocode for single address string (used by bulk uploads / fallback)
|
||||
* Direct forward geocode
|
||||
*/
|
||||
export async function geocodeAddress(address, options = {}) {
|
||||
const suggestions = await getAddressSuggestions(address, { ...options, limit: 1 });
|
||||
|
||||
Reference in New Issue
Block a user