import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', } interface GeocodingRequest { institute_id: string address?: string street?: string town?: string county?: string postcode?: string country?: string } interface SearXNGResponse { query: string number_of_results: number results: Array<{ title: string longitude: string latitude: string boundingbox: string[] geojson?: any osm?: any }> } interface GeocodingResult { success: boolean message: string coordinates?: { latitude: number longitude: number boundingbox: string[] geojson?: any osm?: any } error?: string } serve(async (req: Request) => { // Handle CORS preflight requests if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }) } try { // Get environment variables const supabaseUrl = Deno.env.get('SUPABASE_URL') const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') const searxngUrl = Deno.env.get('SEARXNG_URL') || 'https://search.kevlarai.com' if (!supabaseUrl || !supabaseServiceKey) { throw new Error('Missing required environment variables') } // Create Supabase client const supabase = createClient(supabaseUrl, supabaseServiceKey) // Parse request body const body: GeocodingRequest = await req.json() if (!body.institute_id) { return new Response( JSON.stringify({ error: 'institute_id is required' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } // Get institute data from database const { data: institute, error: fetchError } = await supabase .from('institutes') .select('*') .eq('id', body.institute_id) .single() if (fetchError || !institute) { return new Response( JSON.stringify({ error: 'Institute not found' }), { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } // Build search query from address components let searchQuery = '' if (body.address) { searchQuery = body.address } else { const addressParts = [ body.street, body.town, body.county, body.postcode, body.country ].filter(Boolean) searchQuery = addressParts.join(', ') } // If no search query provided, try to build from institute data if (!searchQuery && institute.address) { const address = institute.address as any const addressParts = [ address.street, address.town, address.county, address.postcode, address.country ].filter(Boolean) searchQuery = addressParts.join(', ') } if (!searchQuery) { return new Response( JSON.stringify({ error: 'No address information available for geocoding' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } // Query SearXNG for geocoding const geocodingResult = await geocodeAddressWithFallback(institute.address, searxngUrl) if (!geocodingResult.success) { return new Response( JSON.stringify({ error: 'Geocoding failed', details: geocodingResult.error }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } // Update institute with geospatial coordinates const { error: updateError } = await supabase .from('institutes') .update({ geo_coordinates: { latitude: geocodingResult.coordinates!.latitude, longitude: geocodingResult.coordinates!.longitude, boundingbox: geocodingResult.coordinates!.boundingbox, geojson: geocodingResult.coordinates!.geojson, osm: geocodingResult.coordinates!.osm, search_query: searchQuery, geocoded_at: new Date().toISOString() } }) .eq('id', body.institute_id) if (updateError) { throw new Error(`Failed to update institute: ${updateError.message}`) } // Log the geocoding operation await supabase .from('function_logs') .insert({ file_id: null, step: 'geocoding', message: 'Successfully geocoded institute address', data: { institute_id: body.institute_id, search_query: searchQuery, coordinates: geocodingResult.coordinates } }) return new Response( JSON.stringify({ success: true, message: 'Institute geocoded successfully', institute_id: body.institute_id, coordinates: geocodingResult.coordinates }), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } catch (error) { console.error('Error in institute geocoder:', error) return new Response( JSON.stringify({ error: 'Internal server error', details: error.message }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } }) async function geocodeAddress(searchQuery: string, searxngUrl: string): Promise { try { console.log(`Geocoding address: ${searchQuery}`) // Build the SearXNG query const query = `!osm ${searchQuery}` const url = `${searxngUrl}/search?q=${encodeURIComponent(query)}&format=json` console.log(`SearXNG URL: ${url}`) const response = await fetch(url) if (!response.ok) { throw new Error(`SearXNG request failed: ${response.status} ${response.statusText}`) } const data: SearXNGResponse = await response.json() console.log(`SearXNG response: ${JSON.stringify(data, null, 2)}`) // Check if we have results if (!data.results || data.results.length === 0) { return { success: false, message: 'No results returned from SearXNG', error: 'No results returned from SearXNG' } } // Get the best result (first one) const bestResult = data.results[0] if (!bestResult.latitude || !bestResult.longitude) { return { success: false, message: 'Result missing coordinates', error: 'Result missing coordinates' } } return { success: true, message: 'Geocoding successful', coordinates: { latitude: parseFloat(bestResult.latitude), longitude: parseFloat(bestResult.longitude), boundingbox: bestResult.boundingbox || [], geojson: bestResult.geojson || null, osm: bestResult.osm || null } } } catch (error) { console.error('Error in geocodeAddress:', error) return { success: false, message: 'Geocoding failed', error: error.message } } } async function geocodeAddressWithFallback(address: any, searxngUrl: string): Promise { // Strategy 1: Try full address (street + town + county + postcode) if (address.street && address.town && address.county && address.postcode) { const fullQuery = `${address.street}, ${address.town}, ${address.county}, ${address.postcode}` console.log(`Trying full address: ${fullQuery}`) const result = await geocodeAddress(fullQuery, searxngUrl) if (result.success) { console.log('Full address geocoding successful') return result } } // Strategy 2: Try town + county + postcode if (address.town && address.county && address.postcode) { const mediumQuery = `${address.town}, ${address.county}, ${address.postcode}` console.log(`Trying medium address: ${mediumQuery}`) const result = await geocodeAddress(mediumQuery, searxngUrl) if (result.success) { console.log('Medium address geocoding successful') return result } } // Strategy 3: Try just postcode if (address.postcode) { console.log(`Trying postcode only: ${address.postcode}`) const result = await geocodeAddress(address.postcode, searxngUrl) if (result.success) { console.log('Postcode geocoding successful') return result } } // Strategy 4: Try town + postcode if (address.town && address.postcode) { const simpleQuery = `${address.town}, ${address.postcode}` console.log(`Trying simple address: ${simpleQuery}`) const result = await geocodeAddress(simpleQuery, searxngUrl) if (result.success) { console.log('Simple address geocoding successful') return result } } // All strategies failed return { success: false, message: 'All geocoding strategies failed', error: 'No coordinates found with any address combination' } }