import { useState, useEffect, useRef } from 'react' import { MapPinIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline' import { nominatimUsStateCode } from '../utils/stateMapping' import { useLocation as useLocationContext, type LocationData, type LocationGranularity } from '../contexts/LocationContext' interface AddressLookupProps { onLocationFound: (location: LocationData) => void initialAddress?: string compact?: boolean } export default function AddressLookup({ onLocationFound, initialAddress = '', compact = false }: AddressLookupProps) { const { clearLocation } = useLocationContext() const [address, setAddress] = useState(initialAddress) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) const [suggestions, setSuggestions] = useState([]) const [foundLocation, setFoundLocation] = useState(null) const [showSuggestions, setShowSuggestions] = useState(false) const [selectedIndex, setSelectedIndex] = useState(-1) const debounceTimer = useRef(null) const inputRef = useRef(null) // Fetch suggestions as user types const fetchSuggestions = async (query: string) => { if (query.trim().length < 3) { setSuggestions([]) setShowSuggestions(false) return } try { // Same-origin proxy (api/routes/geocode.py) β€” avoids Nominatim CORS. const response = await fetch( `/api/geocode/search?q=${encodeURIComponent(query)}&limit=5`, ) if (!response.ok) { return } const data = await response.json() // Deduplicate results using OSM unique IDs const uniqueResults = data.reduce((acc: any[], current: any) => { const osmKey = `${current.osm_type}_${current.osm_id}` const exists = acc.some((item) => { const itemKey = `${item.osm_type}_${item.osm_id}` return itemKey === osmKey }) if (!exists) { acc.push(current) } return acc }, []) setSuggestions(uniqueResults) setShowSuggestions(uniqueResults.length > 0) setSelectedIndex(-1) } catch (err) { console.error('Autocomplete error:', err) } } // Handle address input change with debouncing const handleAddressChange = (value: string) => { setAddress(value) setError(null) // Clear previous timer if (debounceTimer.current) { clearTimeout(debounceTimer.current) } // Set new timer debounceTimer.current = setTimeout(() => { fetchSuggestions(value) }, 300) } // Cleanup timer on unmount useEffect(() => { return () => { if (debounceTimer.current) { clearTimeout(debounceTimer.current) } } }, []) const lookupAddress = async (addressToLookup: string) => { if (!addressToLookup.trim()) { setError('Please enter an address') return } setIsLoading(true) setError(null) setSuggestions([]) setShowSuggestions(false) try { // Use Nominatim (OpenStreetMap) geocoding service const response = await fetch( `https://nominatim.openstreetmap.org/search?` + `q=${encodeURIComponent(addressToLookup)}&` + `format=json&` + `addressdetails=1&` + `countrycodes=us&` + `limit=5`, { headers: { 'User-Agent': 'CommunityOne-Navigator/1.0' } } ) if (!response.ok) { throw new Error('Failed to lookup address') } const data = await response.json() if (data.length === 0) { setError('Address not found. Please try a different address or be more specific.') return } // Deduplicate results using OSM unique IDs const uniqueResults = data.reduce((acc: any[], current: any) => { // Use OSM type + ID as unique key (most reliable) const osmKey = `${current.osm_type}_${current.osm_id}` const exists = acc.some((item) => { const itemKey = `${item.osm_type}_${item.osm_id}` return itemKey === osmKey }) if (!exists) { acc.push(current) } return acc }, []) // If we have multiple unique results, show suggestions if (uniqueResults.length > 1) { setSuggestions(uniqueResults) setShowSuggestions(true) return } // Single result - process it processResult(uniqueResults[0]) } catch (err) { console.error('Address lookup error:', err) setError('Failed to lookup address. Please try again.') } finally { setIsLoading(false) } } const processResult = (result: any) => { const addr = result.address || {} const stateCode = nominatimUsStateCode(addr) || '' console.log(`πŸ—ΊοΈ [AddressLookup] State from Nominatim address: β†’ "${stateCode}"`) const county = (addr.county as string) || '' const city = (addr.city as string) || (addr.town as string) || (addr.village as string) || (addr.municipality as string) || (addr.hamlet as string) || (addr.suburb as string) || '' const hasMunicipality = Boolean(city.trim()) const hasCounty = Boolean(county.trim()) let granularity: LocationGranularity | undefined if (!hasMunicipality) { if (hasCounty) { granularity = 'county' } else if ( result.addresstype === 'state' || (stateCode && result.class === 'boundary' && result.type === 'administrative' && result.addresstype !== 'county') ) { granularity = 'state' } } if (!stateCode) { setError('Could not determine a U.S. state from this place. Try a street address or pick a suggestion from the list.') setSuggestions([]) setShowSuggestions(false) return } if (!hasMunicipality && !granularity) { setError( 'Could not determine a city or county from this place. Try a street address, city name, or pick a more specific suggestion.' ) setSuggestions([]) setShowSuggestions(false) return } const locationData: LocationData = { address: result.display_name, state: stateCode, county, city, granularity, latitude: parseFloat(result.lat), longitude: parseFloat(result.lon), } console.log('πŸ“ [AddressLookup] Location found:', locationData) setSuggestions([]) setShowSuggestions(false) setFoundLocation(locationData) onLocationFound(locationData) } const handleSubmit = (e: React.FormEvent) => { e.preventDefault() // If a suggestion is selected, use that if (selectedIndex >= 0 && suggestions[selectedIndex]) { processResult(suggestions[selectedIndex]) } else { lookupAddress(address) } } const handleSuggestionClick = (suggestion: any) => { setAddress(suggestion.display_name) processResult(suggestion) } const handleKeyDown = (e: React.KeyboardEvent) => { if (!showSuggestions || suggestions.length === 0) return switch (e.key) { case 'ArrowDown': e.preventDefault() setSelectedIndex(prev => prev < suggestions.length - 1 ? prev + 1 : prev ) break case 'ArrowUp': e.preventDefault() setSelectedIndex(prev => prev > 0 ? prev - 1 : -1) break case 'Enter': if (selectedIndex >= 0) { e.preventDefault() processResult(suggestions[selectedIndex]) } break case 'Escape': setShowSuggestions(false) setSelectedIndex(-1) break } } const useMyLocation = () => { if (!navigator.geolocation) { setError('Geolocation is not supported by your browser') return } setIsLoading(true) setError(null) setSuggestions([]) navigator.geolocation.getCurrentPosition( async (position) => { const { latitude, longitude } = position.coords try { // Reverse geocode using Nominatim const response = await fetch( `https://nominatim.openstreetmap.org/reverse?` + `lat=${latitude}&` + `lon=${longitude}&` + `format=json&` + `addressdetails=1`, { headers: { 'User-Agent': 'CommunityOne-Navigator/1.0' } } ) if (!response.ok) { throw new Error('Failed to reverse geocode location') } const data = await response.json() // Update the address input field setAddress(data.display_name) // Process the result processResult(data) } catch (err) { console.error('Reverse geocoding error:', err) setError('Failed to determine your location. Please enter your address manually.') } finally { setIsLoading(false) } }, (error) => { console.error('Geolocation error:', error) setIsLoading(false) switch (error.code) { case error.PERMISSION_DENIED: setError('Location access denied. Please enter your address manually or enable location permissions.') break case error.POSITION_UNAVAILABLE: setError('Location information unavailable. Please enter your address manually.') break case error.TIMEOUT: setError('Location request timed out. Please try again or enter your address manually.') break default: setError('An error occurred while getting your location. Please enter your address manually.') } }, { enableHighAccuracy: false, // Use fast network-based location instead of GPS timeout: 15000, // 15 second timeout to allow time for user to grant permission maximumAge: 30000 // Allow 30s cached location for faster response } ) } if (compact) { return (
handleAddressChange(e.target.value)} onKeyDown={handleKeyDown} placeholder="Enter your address..." className="w-full px-4 py-2 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-900" disabled={isLoading} autoComplete="off" /> {/* Autocomplete suggestions dropdown */} {showSuggestions && suggestions.length > 0 && (
{suggestions.map((suggestion, index) => { const addr = suggestion.address const locationName = addr.city || addr.town || addr.village || addr.county || 'Unknown' return ( ) })}
)}
{error && (

{error}

)}
) } return (
handleAddressChange(e.target.value)} onKeyDown={handleKeyDown} placeholder="123 Main St, Los Angeles, CA 90001" className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-base text-gray-900" disabled={isLoading} autoComplete="off" /> {/* Autocomplete suggestions dropdown */} {showSuggestions && suggestions.length > 0 && (
{suggestions.map((suggestion, index) => { const addr = suggestion.address const locationName = addr.city || addr.town || addr.village || addr.county || 'Unknown' return ( ) })}
)}

We'll find your local organizations based on your address

{/* Use My Location Button */}
{/* Divider */}
or enter manually
{/* Error Message */} {error && (

{error}

)} {/* Note: Suggestions now appear as autocomplete dropdown above */} {/* Location Results */} {foundLocation && !compact && (

Your Local Community

Select a jurisdiction level below to explore organizations, meeting minutes, and contacts:

{/* City */} {foundLocation.city && ( )} {/* County */} {foundLocation.county && ( )} {/* State */} {foundLocation.state && ( )} {/* School District */} {foundLocation.city && ( )}
{/* Action Buttons */}

Quick access to all local resources:

{/* Start Over */}
)}
) }