Source: RouterLocation/geoCode/service.js

// @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' };
};