Class: NWS::Geocoder

Inherits:
Object
  • Object
show all
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.

Examples:

geocoder = NWS::Geocoder.new
result = geocoder.geocode("Denver, CO")
puts result.latitude  # => 39.7392
puts result.longitude # => -104.9903

Constant Summary collapse

NOMINATIM_URL =

Returns Nominatim API endpoint URL.

Returns:

  • (String)

    Nominatim API endpoint URL

"https://nominatim.openstreetmap.org/search"
MIN_REQUEST_INTERVAL =

Returns Minimum seconds between API requests.

Returns:

  • (Float)

    Minimum seconds between API requests

1.0
CACHE_TTL =

Returns Cache time-to-live in seconds (7 days).

Returns:

  • (Integer)

    Cache time-to-live in seconds (7 days)

86400 * 7
ATTRIBUTION =

Returns Attribution text required by ODbL license.

Returns:

  • (String)

    Attribution text required by ODbL license

"Geocoding data © OpenStreetMap contributors (ODbL)".freeze

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(user_agent: nil) ⇒ Geocoder

Initialize a new Geocoder instance

Parameters:

  • user_agent (String, nil) (defaults to: nil)

    User-Agent header for API requests



145
146
147
# File 'lib/nws/geocoder.rb', line 145

def initialize(user_agent: nil)
  @user_agent = user_agent || "(nws-ruby-gem, #{NWS::VERSION})"
end

Class Attribute Details

.cacheHash

Returns In-memory cache storage.

Returns:

  • (Hash)

    In-memory cache storage



40
41
42
# File 'lib/nws/geocoder.rb', line 40

def cache
  @cache
end

.last_request_timeTime?

Returns Timestamp of the last API request.

Returns:

  • (Time, nil)

    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

.attributionString

Get the required attribution text

Returns:

  • (String)

    Attribution text for OpenStreetMap/ODbL



137
138
139
# File 'lib/nws/geocoder.rb', line 137

def attribution
  ATTRIBUTION
end

.cache_dirString

Get the cache directory path

Returns:

  • (String)

    Path to the cache directory (~/.cache/nws by default)



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

Parameters:

  • dir (String)

    New cache directory path

Returns:

  • (String)

    The new 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

Parameters:

  • query (String)

    The geocoding query

Returns:

  • (String)

    Path to the cache file for this 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_dirvoid

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

Parameters:

  • query (String)

    The geocoding query

Returns:

  • (Hash, nil)

    Cached result data or nil if not found or expired



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

Parameters:

  • query (String)

    The geocoding query

  • result (GeocodingResult)

    The result to 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

Parameters:

  • query (String)

    Location name or address to geocode

Returns:

Raises:



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