2025-11-14 14:46:49 +00:00

326 lines
8.9 KiB
TypeScript

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<GeocodingResult> {
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<GeocodingResult> {
// 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'
}
}