import type { LocationLookupResponse } from './types'; import { isCompleteDigiPinInput, isPotentialDigiPinInput } from './digipin'; const DEFAULT_BASE_URL = 'https://api.quantaroute.com'; /** * Cross-platform timeout signal. * AbortSignal.timeout() is not available in React Native / Hermes — fall back * to a manual AbortController + setTimeout when it is missing. */ function makeTimeoutSignal(ms: number): AbortSignal { if ( typeof AbortSignal !== 'undefined' && typeof (AbortSignal as { timeout?: unknown }).timeout === 'function' ) { return AbortSignal.timeout(ms); } const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), ms); return ctrl.signal; } /** * Address components from Nominatim/OpenStreetMap (via reverse geocoding). */ export interface AddressComponents { house_number?: string; road?: string; neighbourhood?: string; suburb?: string; city?: string; state?: string; postcode?: string; country?: string; country_code?: string; name?: string; // Building/Society/POI name addr_housename?: string; addr_place?: string; building?: string; building_name?: string; [key: string]: string | undefined; } /** * Response from /v1/digipin/reverse endpoint. */ export interface ReverseGeocodeResponse { success: boolean; data: { digipin: string; address: string; coordinates: { latitude: number; longitude: number }; confidence: number; displayName: string; addressComponents: AddressComponents; }; error?: string; message?: string; } /** * Call QuantaRoute's Location Lookup API. * Converts lat/lng → full Indian administrative address + DigiPin. */ export async function lookupLocation( lat: number, lng: number, apiKey: string, baseUrl: string = DEFAULT_BASE_URL ): Promise { const url = `${baseUrl.replace(/\/$/, '')}/v1/location/lookup`; let res: Response; try { res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, }, body: JSON.stringify({ latitude: lat, longitude: lng }), signal: makeTimeoutSignal(15_000), }); } catch (err) { if (err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError')) { throw new Error('Request timed out. Check your internet connection.'); } throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`); } if (!res.ok) { let body = ''; try { body = await res.text(); } catch { // ignore } if (res.status === 401 || res.status === 403) { throw new Error('Invalid API key. Check your QuantaRoute API key.'); } if (res.status === 429) { throw new Error('Rate limit exceeded. Upgrade your plan or try again later.'); } throw new Error(`API error ${res.status}: ${body || res.statusText}`); } const data = (await res.json()) as LocationLookupResponse; if (!data.success) { throw new Error(data.message ?? 'Location lookup failed'); } return data; } // ─── Pincode validation (India Post database) ───────────────────────────────── /** One office row from /v1/pincode/validate/:pincode */ export interface PincodeOffice { office_name: string; delivery: string | null; district: string | null; state: string | null; circle: string | null; region: string | null; division: string | null; } export interface PincodeValidateData { isValid: boolean; pincode: string; count: number; offices: PincodeOffice[]; } interface PincodeValidateResponse { success: boolean; data: PincodeValidateData; message?: string; } /** * Validate a 6-digit Indian pincode against QuantaRoute's postal database. * Rejects fake codes (e.g. 123456) that are not in India Post records. */ export async function validatePincode( pincode: string, apiKey: string, baseUrl: string = DEFAULT_BASE_URL ): Promise { const cleaned = pincode.replace(/\D/g, '').slice(0, 6); if (!/^[1-9][0-9]{5}$/.test(cleaned)) { throw new Error('Pincode must be a 6-digit number starting with 1–9.'); } const url = `${baseUrl.replace(/\/$/, '')}/v1/pincode/validate/${cleaned}`; let res: Response; try { res = await fetch(url, { method: 'GET', headers: { 'x-api-key': apiKey }, signal: makeTimeoutSignal(12_000), }); } catch (err) { if (err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError')) { throw new Error('Pincode validation timed out.'); } throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`); } if (!res.ok) { if (res.status === 401 || res.status === 403) { throw new Error('Invalid API key.'); } if (res.status === 429) { throw new Error('Rate limit exceeded.'); } let body = ''; try { body = await res.text(); } catch { // ignore } throw new Error(`Pincode validation failed (${res.status}): ${body || res.statusText}`); } const json = (await res.json()) as PincodeValidateResponse; if (!json.success || !json.data) { throw new Error(json.message ?? 'Pincode validation failed.'); } return json.data; } /** * Whether this pincode has at least one post office with India Post "Delivery" status * (vs "Non Delivery" only — e.g. remote or PO-box-only areas). */ export function pincodeHasDeliveryOffice(offices: PincodeOffice[]): boolean { return offices.some((o) => (o.delivery ?? '').trim().toLowerCase() === 'delivery'); } // ─── Autocomplete ───────────────────────────────────────────────────────────── /** One address suggestion from /v1/digipin/autocomplete */ export interface AutocompleteSuggestion { displayName: string; address: string; coordinates: { latitude: number; longitude: number }; confidence: number; addressComponents: { house_number?: string; road?: string; neighbourhood?: string; suburb?: string; city?: string; state?: string; postcode?: string; country?: string; [key: string]: string | undefined; }; } export interface AutocompleteResponse { success: boolean; data: AutocompleteSuggestion[]; error?: string; message?: string; } /** * Returns true if the trimmed input looks like a complete DigiPin code. * Used to skip the API call and resolve offline instead. */ export function looksLikeDigiPin(input: string): boolean { return isCompleteDigiPinInput(input); } /** * Returns true while the user is typing a DigiPin (compact, dashed, or spaced). * Used to suppress address autocomplete until the code is complete or abandoned. */ export function couldBeDigiPinInput(input: string): boolean { return isPotentialDigiPinInput(input); } /** * Call QuantaRoute's autocomplete API. * Minimum 3 characters required; max 10 suggestions. * * Pass userLat / userLng (the map pin's current position) to unlock: * – Location-biased results (~50 km soft radius around the pin) * – "Near me" / "nearby" keyword support with a tight ~10 km hard radius */ export async function autocompleteAddress( query: string, apiKey: string, baseUrl: string = DEFAULT_BASE_URL, limit = 5, userLat?: number, userLng?: number, ): Promise { let url = `${baseUrl.replace(/\/$/, '')}/v1/digipin/autocomplete` + `?q=${encodeURIComponent(query.trim())}&limit=${Math.min(limit, 10)}`; if (typeof userLat === 'number' && typeof userLng === 'number') { url += `&lat=${userLat.toFixed(6)}&lng=${userLng.toFixed(6)}`; } let res: Response; try { res = await fetch(url, { method: 'GET', headers: { 'x-api-key': apiKey }, signal: makeTimeoutSignal(8_000), }); } catch (err) { if (err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError')) { throw new Error('Autocomplete request timed out.'); } throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`); } if (!res.ok) { if (res.status === 401 || res.status === 403) { throw new Error('Invalid API key.'); } if (res.status === 429) { throw new Error('Rate limit exceeded.'); } return []; // silently return empty on other errors } const data = (await res.json()) as AutocompleteResponse; return data.success ? (data.data ?? []) : []; } /** * Call QuantaRoute's Reverse Geocoding API. * Converts DigiPin → address components from Nominatim/OpenStreetMap. */ export async function reverseGeocode( digipin: string, apiKey: string, baseUrl: string = DEFAULT_BASE_URL ): Promise { const url = `${baseUrl.replace(/\/$/, '')}/v1/digipin/reverse`; let res: Response; try { res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, }, body: JSON.stringify({ digipin }), signal: makeTimeoutSignal(15_000), }); } catch (err) { if (err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError')) { throw new Error('Request timed out. Check your internet connection.'); } throw new Error(`Network error: ${err instanceof Error ? err.message : String(err)}`); } if (!res.ok) { let body = ''; try { body = await res.text(); } catch { // ignore } if (res.status === 401 || res.status === 403) { throw new Error('Invalid API key. Check your QuantaRoute API key.'); } if (res.status === 429) { throw new Error('Rate limit exceeded. Upgrade your plan or try again later.'); } throw new Error(`API error ${res.status}: ${body || res.statusText}`); } const data = (await res.json()) as ReverseGeocodeResponse; if (!data.success) { throw new Error(data.message ?? 'Reverse geocoding failed'); } return data; }