Geocoder gem allows you to preform different operations with coordinates.

Useful usage examples:

  • find latitude and longitude coordinates by address,
  • address by lat-lon coordinates,
  • find all Locations within a square (within_bounding_box)
  • find the distance between two Locations (distance_from)
  • find all Locations around X coordinates (near)
  • find all Locations around X location (nearbys)

Afterwards, when you have the coordinates of a location, you can use a separate Maps API to display them on a map.

Basic geocoder usage #

Geocoder gem does a search via a Places API, and returns coordinates. The more detailed address you search for, the more precise the coordinates you will receive.

bundle add geocoder

# search
ua = Geocoder.search('Kyiv')
ua.first.coordinates
# => [50.4500336, 30.5241361] # latitude and longitude
ua.first.country
# => 'Ukraine'

fr = Geocoder.search('Notre dame cathedral paris')
fr.first.city
# => 'Paris'

# geographic_center
Geocoder::Calculations.geographic_center([ua.first.coordinates, fr.first.coordinates])
# => [50.51223060957045, 16.201193185230583]

Find location from HTTP request #

You can get the current web requests country/ip/etc.

You can use it, for example, to geoblock countries like Ruzzia

# controller or view
request.location
request.location.try(:country)
# request.ip

Geocode a Rails model #

Storing the address as a single string looks like a simple straightforward solution, however storing each address detail separately gives you more power.

A usual address on Google Maps has the sequence street, city, state, country, zip.

Scaffold your location model:

rails g scaffold Location latitude:float:index longitude:float:index street city state country zip
rails g scaffold Location latitude:float:index longitude:float:index address
rails db:migrate

Geocoder will automatically perform a search and find the latitude and longitude of your location.

To save compute power and API thresholds, it makes sence to geocode only if the address has changed.

Basic (one address field):

# app/models/location.rb
  geocoded_by :address   
  after_validation :geocode, if: :address_changed?
  # after_validation :geocode, if: ->(obj){ obj.address.present? and obj.address_changed? }

Advanced (multiple address fields):

# app/models/location.rb
  geocoded_by :address   
  after_validation :geocode, if: :address_changed? 

  def address 
    [street, city, state, country, zip].compact.join(', ')
  end

  private

  def address_changed?
    country_changed? ||
      state_changed? 
      city_changed? ||
      street_changed? ||
      zip_changed? ||
  end 

DEMO DATA: seeds with a few real hotels in France

# db/seeds.rb
name = "HĂ´tel Martinez - The Unbound Collection by Hyatt"
address = "73 Bd de la Croisette, 06400 Cannes"
Location.create(name:, address:)

name = "Exclusive Hotel Belle Plage"
address = "2 Rue Brougham, 06400 Cannes"
Location.create(name:, address:)

name = "Best Western Premier Le Patio des Artistes - Cannes"
address = "6 Rue de BĂ´ne, 06400 Cannes"
Location.create(name:, address:)

name = "Le Negresco"
address = "37 Prom. des Anglais, 06000 Nice"
Location.create(name:, address:)

name = "Caesars Palace"
address = "3570 S Las Vegas Blvd, Las Vegas, NV 89109, United States"
Location.create(name:, address:)

Now you can find call geocoder methods on the model.

# geocode a single record:
address = Address.first
address.geocode
address.save

# geocode all:
Location.all.each { |location| location.geocode && location.save }

Location.geocoded
# => return objects with coordinates
Location.not_geocoded
# => return objects without coordinates

Location.first.to_coordinates
# => [51.51436195, 31.31593525714063]

Location.first.nearbys(20)
Location.first.nearbys(20, units: :km)
# => array of locations within 20 km of coordinates, excluding selected location
# ! useful to show "similar" or "nearby" feature

Location.near(Location.first, 20, units: :km, order: :distance)
Location.near('Omaha, NE, US', 20)
# => all locations within 20 km of coordinates
# ! useful for "find all next to...." feature

Location.first.distance_from(Location.second)
Location.first.distance_to(Location.second) # same as above
Location.first.distance_form([40.714,-100.234])
# => 1.8493403104012456

# all locations within square
sw_corner = [40.71, 100.23] 
ne_corner = [36.12, 88.65]
Location.within_bounding_box(sw_corner, ne_corner)

For example, here’s how you can list all nearby locations within 10km/10mi from current location, and exact distance to them:

# app/views/locations/show.html.erb
<% @location.nearbys(10).each do |location| %>
  <%= location.name %>
  <b>Distance:</b>
  <%= location.distance_to(@location).round(2) %>
  <%= Geocoder.config.units.to_s %>
  <br>
<% end %>

Result:

geocoder-nearby-locations.png

Search locations near address #

Having a search for for place and distance, you can find relevant Locations. This can be a vital feature when building a website like AirBnB or Booking.com.

Example query in human words: Find all locations within 10km distance from Chernihiv, Ukraine

# app/controllers/locations_controller.rb
class LocationsController < ApplicationController
  def index
    if params[:place].present?
      @locations = Location.near(params[:place], params[:distance] || 10, order: :distance)
    else
      @locations = Location.all
    end
  end
end
# a view
<%= form_with url: locations_path, method: :get do |form| %>
  <%= form.label :place, "City, Country" %>
  <%= form.text_field :place, value: params[:place] %>

  <%= label :distance, "Distance, km" %>
  <%= text_field_tag :distance, [10, 20, 30], params[:distance] %>

  <%= form.submit "Search" %>
<% end %>

Result:

geocoder-search-near.png

Display coordinates on static map #

To display a market on a static image map, you would need to connect a places API.

There are a few options for using Places API:

From the above, I’ve tried only Mapbox. As long as you receive a Mapbox API key, you can display the map with an image_tag:

<%= image_tag "https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/pin-s+ff2700(#{location.longitude},#{location.latitude})/#{location.longitude},#{location.latitude},13,0/300x200?access_token=#{Rails.application.credentials.dig(:mapbox_key)}" %>

Result:

geocoder-mapbox-static-map.png

There’s much more that we can do with coordinates. In the future I hope to explore:

  • gem Mapkick for displaying multiple Locations on a responsive map
  • different Geocoding API adapters (like Amazon Location API)
  • get browser location with Javascript

That’s it for now.