export type GeocodeParams = { location?: string latitude?: number longitude?: number label?: string } export type GeocodeResult = { latitude: number longitude: number displayName: string source: 'coordinates' | 'geocoded' query?: string } function clean(value: unknown): string { return String(value || '').trim() } function normalizeCoordinate(value: unknown, label: string): number { const numeric = Number(value) if (!Number.isFinite(numeric)) { throw new Error('Invalid ' + label + ' coordinate.') } return numeric } /** Resolve a free-form place name or direct coordinates into reusable coordinates. */ export async function resolveWeatherLocation( params: GeocodeParams = {}, ): Promise { const input = typeof params === 'object' && params ? params : {} const latitudeProvided = input.latitude !== undefined && input.latitude !== null const longitudeProvided = input.longitude !== undefined && input.longitude !== null if (latitudeProvided || longitudeProvided) { if (!latitudeProvided || !longitudeProvided) { throw new Error( 'Provide both latitude and longitude when using direct coordinates.', ) } const latitude = normalizeCoordinate(input.latitude, 'latitude') const longitude = normalizeCoordinate(input.longitude, 'longitude') return { latitude, longitude, displayName: clean(input.label) || latitude + ', ' + longitude, source: 'coordinates', } } const location = clean(input.location) if (!location) { throw new Error('Provide either a location string or direct coordinates.') } const response = await fetch( 'https://nominatim.openstreetmap.org/search?q=' + encodeURIComponent(location) + '&format=json&limit=1', { headers: { 'User-Agent': 'Kody/1.0 (kody.codes)', }, }, ) const data = await response.json().catch(() => []) if (!response.ok || !Array.isArray(data) || data.length === 0) { throw new Error('Could not geocode location: "' + location + '"') } const first = data[0] return { latitude: Number(first.lat), longitude: Number(first.lon), displayName: clean(first.display_name) || location, source: 'geocoded', query: location, } } export default resolveWeatherLocation