// @ts-check
import fetch from 'node-fetch';
const LOCATIONIQ_API_KEY = process.env.locationIqToken;
const LOCATIONIQ_BASE_URL = 'https://eu1.locationiq.com/v1';
// Rate limiting for LocationIQ free tier (max 2 requests/second, we use 1 to be safe)
const RATE_LIMIT_MS = 1000;
const MAX_QUEUE_SIZE = 10;
let lastRequestTime = 0;
/** @type {Array<{requestFn: Function, resolve: Function, reject: Function}>} */
let requestQueue = [];
let isProcessing = false;
/**
* Rate limiter that ensures we don't exceed LocationIQ free tier limits
* @param {Function} requestFn - The async function to execute
* @returns {Promise<any>}
*/
const rateLimitedRequest = (requestFn) => {
return new Promise((resolve, reject) => {
if (requestQueue.length >= MAX_QUEUE_SIZE) {
reject(new Error('Rate limit queue full. Please try again later.'));
return;
}
requestQueue.push({ requestFn, resolve, reject });
processQueue();
});
};
const processQueue = async () => {
if (isProcessing || requestQueue.length === 0) {
return;
}
isProcessing = true;
while (requestQueue.length > 0) {
const now = Date.now();
const timeSinceLastRequest = now - lastRequestTime;
if (timeSinceLastRequest < RATE_LIMIT_MS) {
await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_MS - timeSinceLastRequest));
}
const { requestFn, resolve, reject } = requestQueue.shift();
lastRequestTime = Date.now();
try {
const result = await requestFn();
resolve(result);
} catch (error) {
reject(error);
}
}
isProcessing = false;
};
/**
* Forward geocode a place name to coordinates
* @param {string} place - Place name or address to geocode
* @returns {Promise<{success: boolean, result?: object, message?: string}>}
*/
export const geocodePlace = async (place) => {
const result = await rateLimitedRequest(async () => {
const url = `${LOCATIONIQ_BASE_URL}/search?key=${LOCATIONIQ_API_KEY}&q=${encodeURIComponent(place)}&format=json`;
const response = await fetch(url);
return await response.json();
});
if (result && result.length > 0) {
return { success: true, result: result[0] };
}
return { success: false, message: 'No results found' };
};
/**
* Reverse geocode coordinates to a place name
* @param {string} lat - Latitude
* @param {string} lng - Longitude
* @returns {Promise<{success: boolean, result?: string, message?: string}>}
*/
export const reverseGeocode = async (lat, lng) => {
const result = await rateLimitedRequest(async () => {
const url = `${LOCATIONIQ_BASE_URL}/reverse?key=${LOCATIONIQ_API_KEY}&lat=${lat}&lon=${lng}&format=json`;
const response = await fetch(url);
return await response.json();
});
if (result && result.display_name) {
return { success: true, result: result.display_name };
}
return { success: false, message: 'No results found' };
};