326 lines
8.9 KiB
TypeScript
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'
|
|
}
|
|
}
|