2025-08-30 17:18:16 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
|
|
module LocationSearch
|
|
|
|
|
class GeocodingService
|
2025-09-01 16:04:55 -04:00
|
|
|
MAX_RESULTS = 10
|
2025-08-30 17:18:16 -04:00
|
|
|
CACHE_TTL = 1.hour
|
|
|
|
|
|
2025-09-03 13:28:26 -04:00
|
|
|
def initialize(query)
|
|
|
|
|
@query = query
|
2025-08-30 17:18:16 -04:00
|
|
|
@cache_key_prefix = 'location_search:geocoding'
|
|
|
|
|
end
|
|
|
|
|
|
2025-09-03 13:28:26 -04:00
|
|
|
def search
|
2025-08-30 17:18:16 -04:00
|
|
|
return [] if query.blank?
|
|
|
|
|
|
|
|
|
|
cache_key = "#{@cache_key_prefix}:#{Digest::SHA256.hexdigest(query.downcase)}"
|
2025-09-03 12:51:00 -04:00
|
|
|
|
2025-08-30 17:18:16 -04:00
|
|
|
Rails.cache.fetch(cache_key, expires_in: CACHE_TTL) do
|
|
|
|
|
perform_geocoding_search(query)
|
|
|
|
|
end
|
|
|
|
|
rescue StandardError => e
|
|
|
|
|
Rails.logger.error "Geocoding search failed for query '#{query}': #{e.message}"
|
|
|
|
|
[]
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def provider_name
|
|
|
|
|
Geocoder.config.lookup.to_s.capitalize
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
2025-09-03 13:28:26 -04:00
|
|
|
attr_reader :query
|
2025-09-03 12:51:00 -04:00
|
|
|
|
2025-09-03 13:28:26 -04:00
|
|
|
def perform_geocoding_search(query)
|
2025-08-30 17:18:16 -04:00
|
|
|
results = Geocoder.search(query, limit: MAX_RESULTS)
|
|
|
|
|
return [] if results.blank?
|
|
|
|
|
|
2025-09-03 13:28:26 -04:00
|
|
|
normalize_geocoding_results(results)
|
2025-08-30 17:18:16 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def normalize_geocoding_results(results)
|
|
|
|
|
normalized_results = []
|
2025-09-03 12:51:00 -04:00
|
|
|
|
2025-09-03 13:28:26 -04:00
|
|
|
results.each do |result|
|
|
|
|
|
next unless valid_result?(result)
|
2025-08-30 17:18:16 -04:00
|
|
|
|
|
|
|
|
normalized_result = {
|
|
|
|
|
lat: result.latitude.to_f,
|
|
|
|
|
lon: result.longitude.to_f,
|
2025-09-03 13:28:26 -04:00
|
|
|
name: result.address&.split(',')&.first || 'Unknown location',
|
|
|
|
|
address: result.address || '',
|
|
|
|
|
type: result.data&.dig('type') || result.data&.dig('class') || 'unknown',
|
|
|
|
|
provider_data: {
|
|
|
|
|
osm_id: result.data&.dig('osm_id'),
|
|
|
|
|
place_rank: result.data&.dig('place_rank'),
|
|
|
|
|
importance: result.data&.dig('importance')
|
|
|
|
|
}
|
2025-08-30 17:18:16 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
normalized_results << normalized_result
|
|
|
|
|
end
|
|
|
|
|
|
2025-09-03 13:28:26 -04:00
|
|
|
deduplicate_results(normalized_results)
|
2025-08-30 17:18:16 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def valid_result?(result)
|
2025-09-03 12:51:00 -04:00
|
|
|
result.present? &&
|
|
|
|
|
result.latitude.present? &&
|
2025-08-30 17:18:16 -04:00
|
|
|
result.longitude.present? &&
|
|
|
|
|
result.latitude.to_f.abs <= 90 &&
|
|
|
|
|
result.longitude.to_f.abs <= 180
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def deduplicate_results(results)
|
|
|
|
|
deduplicated = []
|
2025-09-03 12:51:00 -04:00
|
|
|
|
2025-08-30 17:18:16 -04:00
|
|
|
results.each do |result|
|
|
|
|
|
# Check if there's already a result within 100m
|
|
|
|
|
duplicate = deduplicated.find do |existing|
|
|
|
|
|
distance = calculate_distance(
|
|
|
|
|
result[:lat], result[:lon],
|
|
|
|
|
existing[:lat], existing[:lon]
|
|
|
|
|
)
|
|
|
|
|
distance < 100 # meters
|
|
|
|
|
end
|
2025-09-03 12:51:00 -04:00
|
|
|
|
2025-08-30 17:18:16 -04:00
|
|
|
deduplicated << result unless duplicate
|
|
|
|
|
end
|
2025-09-03 12:51:00 -04:00
|
|
|
|
2025-08-30 17:18:16 -04:00
|
|
|
deduplicated
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def calculate_distance(lat1, lon1, lat2, lon2)
|
|
|
|
|
# Haversine formula for distance calculation in meters
|
|
|
|
|
rad_per_deg = Math::PI / 180
|
2025-09-03 12:51:00 -04:00
|
|
|
rkm = 6_371_000 # Earth radius in meters
|
|
|
|
|
|
2025-08-30 17:18:16 -04:00
|
|
|
dlat_rad = (lat2 - lat1) * rad_per_deg
|
|
|
|
|
dlon_rad = (lon2 - lon1) * rad_per_deg
|
2025-09-03 12:51:00 -04:00
|
|
|
|
2025-08-30 17:18:16 -04:00
|
|
|
lat1_rad = lat1 * rad_per_deg
|
|
|
|
|
lat2_rad = lat2 * rad_per_deg
|
2025-09-03 12:51:00 -04:00
|
|
|
|
2025-08-30 17:18:16 -04:00
|
|
|
a = Math.sin(dlat_rad / 2)**2 + Math.cos(lat1_rad) * Math.cos(lat2_rad) * Math.sin(dlon_rad / 2)**2
|
|
|
|
|
c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
2025-09-03 12:51:00 -04:00
|
|
|
|
2025-08-30 17:18:16 -04:00
|
|
|
rkm * c
|
|
|
|
|
end
|
2025-08-31 06:08:33 -04:00
|
|
|
|
2025-08-30 17:18:16 -04:00
|
|
|
end
|
2025-09-03 12:51:00 -04:00
|
|
|
end
|