Rails|Ransack|Geocoder – Sort data by distance

If your application connects local businesses to local customer, then you might have used Ransack for sorting and searching of records. For geocoding and reverse-geocoding, you might be using Geocoder gem. If you want to let users to sort results by distance then its pretty easy.

Things to remember,

  • If you use Model.near([lat, long]) method, Ransack adds a new column called distance to the ActiverecordRelation object. You can access the method distance by prying.

Continue reading

Rails : Auto updating geo-coded object attributes

Lets say you are setting your model such that, user only needs to set / input their zipcode and whenever they modify the zipcode the application needs to re-calculate the address information like country, city, state, etc.

# in geo-codable model
geocoded_by :zip_code

after_validation :geocode, if: ->(obj){ obj.zip_code.present? && obj.zip_code_changed? }
after_validation :reverse_geocode, if: ->(obj){ obj.zip_code.present? && obj.zip_code_changed? }

reverse_geocoded_by :latitude, :longitude do |obj, results|
  if geo = results.first
    obj.state = geo.state
    obj.city = geo.city

In the setup above, zip_code field in the table is used to gather the geo-coordinates which will be stored in lat and long field. This is forward geo-coding.

And whenever zip_code changes, we need to recalculate the long, lat . Also we need to re-update the state and city information.

So, the code above does the stuff smoothly.


Introduction to Geo-Coding in Rails


Geocoding typically refers to the transformation process of addresses and places to coordinates.

# Example
Geocoder.coordinates("25 Main St, Cooperstown, NY")
 => [42.700149, -74.922767]

It is sometimes called forward geocoding whereas Reverse geocoding uses geographic coordinates to find a description of the location, most typically a postal address or place name.

In Rails we are going to use Geocoder gem.

Geocoder [Reference Inspired from Original Doc]

Geocoder is a complete geocoding solution for Ruby. With Rails it adds geocoding (by street or IP address), reverse geocoding (finding street address based on given coordinates), and distance queries. It’s as simple as calling geocode on your objects, and then using a scope like Venue.near("Billings, MT").

For geocoding your model must provide a method that returns an address. This can be a single attribute, but it can also be a method that returns a string assembled from different attributes (eg: city, state, and country).

Next, your model must tell Geocoder which method returns your object’s geocodable address:

geocoded_by :full_street_address   # can also be an IP address
after_validation :geocode          # auto-fetch coordinates

For reverse geocoding, tell Geocoder which attributes store latitude and longitude:

reverse_geocoded_by :latitude, :longitude
after_validation :reverse_geocode  # auto-fetch address

Request Geocoding by Zip-Code

geocoded_by :zip_code

reverse_geocoded_by :latitude, :longitude do |obj, results|
  if geo = results.first
    obj.state = geo.state
    obj.city = geo.city


Request Geocoding by IP Address

Geocoder adds location and safe_location methods to the standard Rack::Request object so you can easily look up the location of any HTTP request by IP address. For example, in a Rails controller or a Sinatra app:

# returns Geocoder::Result object
result = request.location

The location method is vulnerable to trivial IP address spoofing via HTTP headers. If that’s a problem for your application, use safe_location instead.

Location-Aware Database Queries

For geocoded objects you can do things like this:

if obj.geocoded?
  obj.nearbys(30)                      # other objects within 30 miles
  obj.distance_from([40.714,-100.234]) # distance from arbitrary point to object
  obj.bearing_to("Paris, France")      # direction from object to arbitrary point


At class level

Venue.near('Omaha, NE, US', 20)    # venues within 20 miles of Omaha
Venue.near([40.71, -100.23], 20)    # venues within 20 miles of a point
Venue.near([40.71, -100.23], 20, :units => :km)
                                   # venues within 20 kilometres of a point
Venue.geocoded                     # venues with coordinates
Venue.not_geocoded                 # venues without coordinates

Here Venue is a model in which geocoder is initialized.

Distance and Bearing

When you run a location-aware query the returned objects have two attributes added to them (only w/ ActiveRecord):

  • obj.distance – number of miles from the search point to this object
  • obj.bearing – direction from the search point to this object

Bearing is given as a number of clockwise degrees from due north, for example:

  • 0 – due north
  • 180 – due south
  • 90 – due east
  • 270 – due west
  • compass-silver-direction-north-south-east-west-34574249230.1 – southwest
  • 359.9 – almost due north






You can convert these numbers to compass point names by using the utility method provided:

Geocoder::Calculations.compass_point(355) # => "N"
Geocoder::Calculations.compass_point(45)  # => "NE"
Geocoder::Calculations.compass_point(208) # => "SW"

when using SQLite distance and bearing values are provided for interface consistency only. They are not very accurate.

To calculate accurate distance and bearing with SQLite or MongoDB:

obj.distance_to([43.9,-98.6])  # distance from obj to point
obj.bearing_to([43.9,-98.6])   # bearing from obj to point
obj.bearing_from(obj2)         # bearing from obj2 to obj

The bearing_from/to methods take a single argument which can be: a [lat,lon] array, a geocoded object, or a geocodable address (string). The distance_from/to methods also take a units argument (:mi, :km, or :nm for nautical miles).

Search in Box rather than Circle

You can also look within a square rather than a radius (circle) by using the within_bounding_box scope:

distance = 20
center_point = [40.71, 100.23]
box = Geocoder::Calculations.bounding_box(center_point, distance)
  • Note, however, that returned results do not include distance and bearing attributes.
  • Note that#near performs both bounding box and radius queries for speed.

Search in Donut or Ring

You can also specify a minimum radius (if you’re using ActiveRecord and not Sqlite) to constrain the lower bound (ie. think of a donut, or ring) by using the :min_radius option:

box = Geocoder::Calculations.bounding_box(center_point, distance, :min_radius => 10.5)


Auto Assignment of decoded info

So far we have looked at shortcuts for assigning geocoding results to object attributes.

However, if you need to do something fancy you can skip the auto-assignment by providing a block (takes the object to be geocoded and an array ofGeocoder::Result objects) in which you handle the parsed geocoding result any way you like, for example:

reverse_geocoded_by :latitude, :longitude do |obj,results|
  if geo = results.first
    obj.city    = geo.city
    obj.zipcode = geo.postal_code
    obj.country = geo.country_code
after_validation :reverse_geocode

Every Geocoder::Result object, result, provides the following data:

  • result.latitude – float
  • result.longitude – float
  • result.coordinates – array of the above two in the form of [lat,lon]
  • result.address – string
  • result.city – string
  • result.state – string
  • result.state_code – string
  • result.postal_code – string
  • result.country – string
  • result.country_code – string