Class: NWS::Geocoder
- Inherits:
-
Object
- Object
- NWS::Geocoder
- Defined in:
- lib/nws/geocoder.rb
Overview
Geocoder for converting location names to coordinates using OpenStreetMap Nominatim
Implements rate limiting (1 request/second) and caching (7-day TTL) to comply with Nominatim usage policy.
Constant Summary collapse
- NOMINATIM_URL =
Returns Nominatim API endpoint URL.
"https://nominatim.openstreetmap.org/search"- MIN_REQUEST_INTERVAL =
Returns Minimum seconds between API requests.
1.0- CACHE_TTL =
Returns Cache time-to-live in seconds (7 days).
86400 * 7
- ATTRIBUTION =
Returns Attribution text required by ODbL license.
"Geocoding data © OpenStreetMap contributors (ODbL)".freeze
Class Attribute Summary collapse
-
.cache ⇒ Hash
In-memory cache storage.
-
.last_request_time ⇒ Time?
Timestamp of the last API request.
Class Method Summary collapse
-
.attribution ⇒ String
Get the required attribution text.
-
.cache_dir ⇒ String
Get the cache directory path.
-
.cache_dir=(dir) ⇒ String
Set the cache directory path.
-
.cache_path(query) ⇒ String
Generate the cache file path for a query.
-
.ensure_cache_dir ⇒ void
Ensure the cache directory exists.
-
.rate_limit! ⇒ void
Enforce rate limiting by sleeping if necessary.
-
.read_cache(query) ⇒ Hash?
Read a cached geocoding result.
-
.write_cache(query, result) ⇒ void
Write a geocoding result to the cache.
Instance Method Summary collapse
-
#geocode(query) ⇒ GeocodingResult
Geocode a location query to coordinates.
-
#initialize(user_agent: nil) ⇒ Geocoder
constructor
Initialize a new Geocoder instance.
Constructor Details
Class Attribute Details
.cache ⇒ Hash
Returns In-memory cache storage.
40 41 42 |
# File 'lib/nws/geocoder.rb', line 40 def cache @cache end |
.last_request_time ⇒ Time?
Returns Timestamp of the last API request.
37 38 39 |
# File 'lib/nws/geocoder.rb', line 37 def last_request_time @last_request_time end |
Class Method Details
.attribution ⇒ String
Get the required attribution text
137 138 139 |
# File 'lib/nws/geocoder.rb', line 137 def attribution ATTRIBUTION end |
.cache_dir ⇒ String
Get the cache directory path
61 62 63 |
# File 'lib/nws/geocoder.rb', line 61 def cache_dir @cache_dir ||= File.join(Dir.home, ".cache", "nws") end |
.cache_dir=(dir) ⇒ String
Set the cache directory path
69 70 71 |
# File 'lib/nws/geocoder.rb', line 69 def cache_dir=(dir) @cache_dir = dir end |
.cache_path(query) ⇒ String
Generate the cache file path for a query
84 85 86 87 |
# File 'lib/nws/geocoder.rb', line 84 def cache_path(query) safe_name = query.downcase.gsub(/[^a-z0-9]+/, "_")[0..50] File.join(cache_dir, "geocode_#{safe_name}.json") end |
.ensure_cache_dir ⇒ void
This method returns an undefined value.
Ensure the cache directory exists
76 77 78 |
# File 'lib/nws/geocoder.rb', line 76 def ensure_cache_dir FileUtils.mkdir_p(cache_dir) unless File.directory?(cache_dir) end |
.rate_limit! ⇒ void
This method returns an undefined value.
Enforce rate limiting by sleeping if necessary
Ensures at least MIN_REQUEST_INTERVAL seconds between API requests to comply with Nominatim usage policy.
48 49 50 51 52 53 54 55 56 |
# File 'lib/nws/geocoder.rb', line 48 def rate_limit! if last_request_time elapsed = Time.now - last_request_time if elapsed < MIN_REQUEST_INTERVAL sleep(MIN_REQUEST_INTERVAL - elapsed) end end self.last_request_time = Time.now end |
.read_cache(query) ⇒ Hash?
Read a cached geocoding result
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/nws/geocoder.rb', line 93 def read_cache(query) path = cache_path(query) return nil unless File.exist?(path) data = JSON.parse(File.read(path)) cached_at = Time.parse(data["cached_at"]) if Time.now - cached_at > CACHE_TTL File.delete(path) rescue nil return nil end data["result"] rescue JSON::ParserError, ArgumentError File.delete(path) rescue nil nil end |
.write_cache(query, result) ⇒ void
This method returns an undefined value.
Write a geocoding result to the cache
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/nws/geocoder.rb', line 116 def write_cache(query, result) ensure_cache_dir path = cache_path(query) data = { "cached_at" => Time.now.iso8601, "query" => query, "result" => { "latitude" => result.latitude, "longitude" => result.longitude, "display_name" => result.display_name, "place_type" => result.place_type } } File.write(path, JSON.pretty_generate(data)) rescue StandardError # Ignore cache write failures end |
Instance Method Details
#geocode(query) ⇒ GeocodingResult
Geocode a location query to coordinates
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/nws/geocoder.rb', line 155 def geocode(query) # Check cache first if (cached = self.class.read_cache(query)) return GeocodingResult.new( latitude: cached["latitude"], longitude: cached["longitude"], display_name: cached["display_name"], place_type: cached["place_type"], from_cache: true ) end # Rate limit before making request self.class.rate_limit! uri = URI.parse(NOMINATIM_URL) uri.query = URI.encode_www_form(format: "json", q: query, limit: 1) request = Net::HTTP::Get.new(uri) request["User-Agent"] = @user_agent request["Accept"] = "application/json" http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.open_timeout = 10 http.read_timeout = 30 response = http.request(request) unless response.code.to_i == 200 raise APIError.new("Geocoding failed: #{response.code}", status_code: response.code.to_i) end results = JSON.parse(response.body) if results.empty? raise NotFoundError.new("Location not found: #{query}") end result_data = results.first lat = result_data["lat"].to_f.round(4) lon = result_data["lon"].to_f.round(4) result = GeocodingResult.new( latitude: lat, longitude: lon, display_name: result_data["display_name"], place_type: result_data["type"], from_cache: false ) # Cache the result self.class.write_cache(query, result) result end |