Geocoder a way rails support geocoding cover

A few weeks ago I run a spontaneous trip to Toruń. Once I reached my destination I realized how easy it was to find a proper place to park or to plan a short coffee break while driving. All thanks to a small mobile device stuck to the front glass of my car. I realized that geolocalization is a crucial part of many services in a nowadays world. Let’s see how Rails allows you to make use of this technology.

This article explains how to use a geocoder gem and what are the main benefits of introducing it into your app.

Installation and basic usage

The gem itself can be found here. As you can see the gem is being maintained actively (at least, at the day of writing this) so you should not be concerned with any compatibility issues. Firstly let’s just install the gem and grasp the first feeling of what you can do.

~ gem install geocoder
~ irb

2.4.0 :001 > require 'Geocoder'
 => true
2.4.0 :002 > Geocoder.search('Piotrkowska 99, Lodz')

You will probably see a hash representing detailed information about the place. It may be helpful in case you need to present a place on a map. However, you might not be interested in all of them. Let's be honest, the key feature is to convert an address into coordinates and the other way round. To accomplish this we can use address and coordinates methods.

2.4.0 :003 > Geocoder.coordinates('Piotrkowska 99, Lodz')
 => [51.765142, 19.4568799]
2.4.0 :005 > Geocoder.address([51.7651, 19.45687])
 => "Piotrkowska 99, Łódź, Poland"

Another method worthy of mention is distance_between. It allows you to calculate the distance between two points.

2.4.0 :014 > Geocoder::Calculations.distance_between([51.7651, 19.45687], 'Aleje Jerozolimskie 45, Warsaw, Poland', units: :km)
 => 118.2082597519023

Points can be represented either by an array of coordinates or an address. You can also pass options hash to a method. In the above case, we define the unit of the result. Keep in mind that the distance is calculated in a straight line, not using available routes.

If you are wondering where all that data comes from you can check the configuration.

2.4.0 :016 > Geocoder.config
 => {
  :timeout=>3,
  :lookup=>:google,
  :ip_lookup=>:freegeoip,
  :language=>:en,
  :http_headers=>{},
  :use_https=>false,
  :http_proxy=>nil,
  :https_proxy=>nil,
  :api_key=>nil,
  :cache=>nil,
  :cache_prefix=>"geocoder:",
  :basic_auth=>{},
  :logger=>:kernel,
  :kernel_logger_level=>2,
  :always_raise=>[],
  :units=>:mi,
  :distances=>:linear
}

The default data provider (lookup service) is Google Maps Geocoding API. Fortunately, it offers some basic functionalities free of charge. In this tier, you dispose of 2,500 requests per 24 hours and 5 requests per second quotas. If you would like to increase your limits you have to sign up to obtain an API key. Geocoder supports also a number of other lookup services, including Bing, Nominatim (OpenStreetMap), Yandex, Mapbox and much more.

Let's take a look at three other configuration options. timeout (in seconds) defines how long the system waits for a response from the lookup service, units defines a data format for all returned results and distances that covers calculation method. It can be linear or spherical. The latter one takes into account the curvature of the earth surface. Thanks to that results are more accurate, especially when checking the distance between two points far away from each other.

Rails implementation

Now let's see what are main the benefits of the library in a Rails application environment.

Imagine you run a huge E-commerce company selling goods all over the world and you maintain a number of stores and warehouses located in many different cities. Sooner or later you will face many problems relating to logistics that will lead you to an idea of creating a dedicated system that will deal with following issues:

  • uploading a list of stores from CSV file to geocode it and present it on a map
  • calculating distances between stores and warehouses that supply goods to those stores
  • finding the closest store regarding the present user location

At the beginning let's include the gem in the Gemfile.

gem 'geocoder'

After running the bundle install command gem is installed and the default configuration is set.

Now let's create some structure by adding a Shop model to our application.

rails g model shop name:string address:string latitude:decimal longitude:decimal

longitude and latitude attributes will hold the place's coordinates.

Now the key part. Geocoder allows you to automate the process of adding places a little bit. Instead of geocoding places by hand you can do it via a callback method. To achieve that you need to add just a few lines to your model.

class Shop < ApplicationRecord
  geocoded_by :address
  reverse_geocoded_by :latitude, :longitude
  after_validation :geocode, if: ->(obj){ obj.address_changed? }

  private

  def address_changed?
    address.present? && address_changed?
  end
end

geocoded_by callback indicates what data is to be geocoded. This can be a single attribute, but it can also be a method that combines different attributes and returns a string.

As you can see, we set after_validation callback that will call geocode method to fetch coordinates based on the address. To avoid unnecessary API requests (and quota usage) when updating an object but not changing an address you can use a slightly more complicated callback that uses a lambda or a proc to add a condition.

after_validation :geocode, if: ->(obj){ obj.address.present? and obj.address_changed? }

Now after creating a new Shop, it will get proper coordinates.

2.4.0 :004 > Shop.create(name: 'test', address: 'Piotrkowska 100, Lodz, Poland')
 => #<Shop id: 1, name: "test", address: "Piotrkowska 100, Lodz, Poland", latitude: 0.517653274e2, longitude: 0.194574084e2, created_at: "2017-08-24 11:00:28", updated_at: "2017-08-24 11:00:28">

2.4.0 :007 > Shop.first.latitude.to_f
 => 51.7653274
2.4.0 :008 > Shop.first.longitude.to_f
 => 19.4574084

If you want to perform reversed geocoding you need to enhance your model a little bit.

class Shop < ApplicationRecord
  geocoded_by :address
  reverse_geocoded_by :latitude, :longitude
  after_validation :geocode, if: ->(obj){ obj.address_changed? }
  after_validation :reverse_geocode, if: ->(obj){ obj.coords_changed? }

  private

  def address_changed?
    address.present? && address_changed?
  end

  def coords_changed?
    latitude.present? &&
    longitude.present? &&
    latitude_changed? &&
    longitude_changed?
  end
end

The above example should work disregarding what data (address or coordinates) is provided while creating a shop.

2.4.0 :023 > Shop.create(name: 'Second shop', latitude: 51.10012, longitude: 20.798378)
 => #<Shop id: 7, name: "Second shop", address: "Bugaj 2, 26-120 Bugaj, Poland", latitude: 0.5110012e2, longitude: 0.20798378e2, created_at: "2017-08-24 11:21:15", updated_at: "2017-08-24 11:21:15">

2.4.0 :024 > Shop.last.address
 => "Bugaj 2, 26-120 Bugaj, Poland"
2.4.0 :025 >

Now we have everything to perform more advanced location-aware database queries. We can, for example, fetch all shops within 20 miles of our present location (you can provide either an address or coordinates).

2.4.0 :041 > Shop.near('Piotrkowska 99, Lodz, Poland', 20).map do |s|
  { address: s.address, distance: s.distance.round(2), bearing: Geocoder::Calculations.compass_point(s.bearing) }
end
 => [
  {:address=>"Piotrkowska 100, Łódź, Poland", :distance=>0.03, :bearing=>"E"},
  {:address=>"Goplańska 23, Łódź, Poland", :distance=>1.84, :bearing=>"N"},
  {:address=>"Smocza 19/21, Łódź, Poland", :distance=>2.39, :bearing=>"S"},
  {:address=>"Rokicińska 20, Łódź, Poland", :distance=>2.9, :bearing=>"E"},
  {:address=>"Stare Złotno 74, Łódź, Poland", :distance=>3.93, :bearing=>"W"}
]

Geocoder adds two special attributes to every location-aware query: distance and bearing. Thanks to that you can easily obtain how far each shop is and in what direction. By default results are ordered by distance. With that you are ready to present a list of the closest shops regarding the present user location.

If you realize that one of your shops is out of desired goods you can quickly find another one in the neighborhood.

2.4.0 :060 > Shop.last.nearbys(1).map{ |s| { address: s.address, dist: s.distance.round(2) } }
 => [{:address=>"Rokicińska 20, Łódź, Poland", :dist=>0.52}]

You can also check the distance between two shops.

2.4.0 :062 > Shop.first.distance_to(Shop.last).round(2)
 => 3.36

The library gives much more possibilities of using geo-local data. I suggest reading the gem README to get more familiar with advanced functionalities.

Configuration

It is possible to generate an initializer with the default configuration.

~ rails generate geocoder:config

config/initializers/geocoder.rb

Geocoder.configure(
  timeout: 3,                 # geocoding service timeout (secs)
  lookup: :google,            # name of geocoding service (symbol)
  ip_lookup: :freegeoip,      # name of IP address geocoding service (symbol)
  language: :en,              # ISO-639 language code
  use_https: false,           # use HTTPS for lookup requests? (if supported)
  http_proxy: nil,            # HTTP proxy server (user:pass@host:port)
  https_proxy: nil,           # HTTPS proxy server (user:pass@host:port)
  api_key: nil,               # API key for geocoding service
  cache: nil,                 # cache object (must respond to #[], #[]=, and #del)
  cache_prefix: 'geocoder:',  # prefix (string) to use for all cache keys
  always_raise: [],
  units: :mi,                 # :km for kilometers or :mi for miles
  distances: :linear          # :spherical or :linear
)

Now you can change a lookup service or distance units. It is worthy of mention about a cache parameter. Regarding the fact that we rely on an external service and we have a certain limit for performing requests it is vital to cache the results if possible. It's easy to cache geocoding results with Geocoder. You can use Redis as the cache store.

Testing

When testing your models you probably would like to avoid any external calls as they may slow down your test suite execution. It is possible to stub geocoder objects.

Geocoder.configure(:lookup => :test)

Geocoder::Lookup::Test.add_stub(
  'Lodz, Poland', [
    {
      'latitude'     => 51.7592485,
      'longitude'    => 19.4559833,
      'address'      => 'Lodz, Poland',
      'state'        => 'Lodz',
      'state_code'   => 'Lodz',
      'country'      => 'Poland',
      'country_code' => 'Pl'
    }
  ]
)

Now, any time you tries to geocode 'Lodz, Poland' Geocoder will return the above array. You can also add a default stub that will be returned in case any other stub isn't found. To do that just replace add_stub with set_default_stub.

Summary

That's it. With little effort, we can build a system that deals with many serious problems related to localization services. Thanks to Geocoder, configuration stuff is reduced to the minimum and the usage is as simple as that. With the gem, not only can you calculate some distances very quickly but also build a complete localization system.

Post tags:

Join our awesome team
Check offers

Work
with us

Tell us about your idea
and we will find a way
to make it happen.

Get estimate