Geocoder Dos and Do Nots
Tags: geocoder, ruby, rails
Published: 2020-01-21 19:01:35

---
title: Geocoder Dos and Do Nots
published: true
description: Advice on how to use the popular Ruby Geocoder gem.
tags: geocoder, ruby, rails
---

I was working on a Rails project recently where we needed to 1) track the location of fine art delivery trucks and 2) give dispatchers visibility into which trucks were closest to a given location. Perfect use case for the [Ruby `geocoder` gem](https://github.com/alexreisner/geocoder), right?

This gem is a long-respected fan favorite in the Ruby community, first [released in 2009](https://github.com/alexreisner/geocoder/releases?after=0.8.7) and [used by upwards of 23.5k projects](https://github.com/alexreisner/geocoder/network/dependents?package_id=UGFja2FnZS02MjgyNQ%3D%3D). However, this was my first time working with it, and I managed to bungle my first implementation. I wanted to share my journey from sad path to happy path, in hopes that it'll help someone else out there, too.

## Setup

We're starting with the following schema:

```ruby
# app/models/truck.rb
class Truck < ActiveRecord::Base
  def location
    # TODO
  end

  def set_location(latitude:, longitude:)
    # TODO
  end
end

# schema.rb
ActiveRecord::Schema.define(version: 20191008222320) do
  create_table "trucks", force: :cascade do |t|
    t.string "name", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
end
```

## What Didn't Work 🙅‍♀️

For my first pass, I opted to create a separate `locations` table. The idea was to maintain separation of concerns between `trucks` and `locations`, keeping them loosely coupled so they'd both be easier to extend (or deprecate, in case this whole truck tracking idea didn't work out).

I also didn't originally like the idea of tacking `latitude` and `longitude` on to the existing `trucks` table. Those didn't seem like inherit properties of a truck; instead, in my mind, a truck has many locations, current and past. Another bonus of a separate `locations` table is that it allows us to keep track of a truck's past locations, which also seemed appealing to me (in case we wanted to recreate a timeline of the truck's movements, for example).

So with that rationale in mind, I added a `locations` table, model, and associations:

```ruby
# app/models/location.rb
class Location < ActiveRecord::Base
  belongs_to :truck
end

# app/models/truck.rb
class Truck < ActiveRecord::Base
  has_many :locations

  def address
    # TODO
  end

  def location
    return nil if locations.empty?
    most_recent_location = locations.order(created_at: :desc).first

    [most_recent_location.latitude, most_recent_location.longitude]
  end

  def set_location(latitude:, longitude:)
    locations.create(latitude: latitude, longitude: longitude)
  end
end

# schema.rb
ActiveRecord::Schema.define(version: 20191008351012) do
  create_table "trucks", force: :cascade do |t|
    # same as above
  end

  create_table "locations", force: :cascade do |t|
    t.integer "truck_id", null: false
    t.decimal "latitude", precision: 10, scale: 6, null: false
    t.decimal "longitude", precision: 10, scale: 6, null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["truck_id"], name: "index_locations_on_truck_id"
  end
end
```

Next, I checked the [geocoder docs](https://github.com/alexreisner/geocoder#geocoding-objects). Per the docs, there's four things necessary to geocode an object:

**1. Provide a method that returns an address to geocode**

I'd read somewhere it was ok to stub this out to return nil, so I opted to start there, since knowing a truck's address wasn't one of our requirements.

**2. Provide a way to store latitude and longitude coordinates.**

The docs suggest adding `:latitude` and `:longitude` attributes to the model you're geocoding (in our case, `Truck`), but I figured I could overwrite these by adding methods that hooked into the associated `Location` attributes instead.

**3. Tell geocoder where to find the object's address**

I opted to start with the default examples provided in the docs: `geocoded_by :address`

**4. Add `after_validation :geocode` to model**

...all of which lead to the following updates to my `Truck` model:

```ruby
# app/models/truck.rb
class Truck < ActiveRecord::Base
  has_many :locations

  geocoded_by :address
  after_validation :geocode

  def address
    nil # stubbed, not necessary for our requirements
  end

  def latitude
    location.try(:first)
  end

  def longitude
    location.try(:last)
  end

  def location
    return nil if locations.empty?
    most_recent_location = locations.order(created_at: :desc).first
    [most_recent_location.latitude, most_recent_location.longitude]
  end

  def set_location(latitude:, longitude:)
    locations.create(latitude: latitude, longitude: longitude)
  end
end
```

This configuration worked fine in terms of setting and getting location:

```ruby
irb(main):001:0> truck = Truck.first
  Truck Load (0.1ms)  SELECT  "trucks".* FROM "trucks" ORDER BY "trucks"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<Truck id: 1, name: "Maquette Fine Art Services", created_at: "2019-10-10 04:10:58", updated_at: "2019-10-10 04:10:58">
irb(main):002:0> truck.location
  Location Exists (0.2ms)  SELECT  1 AS one FROM "locations" WHERE "locations"."truck_id" = ? LIMIT ?  [["truck_id", 1], ["LIMIT", 1]]
=> nil
irb(main):003:0> truck.set_location(latitude: 37.782267, longitude: -122.391248)
   (0.2ms)  begin transaction
  SQL (2.9ms)  INSERT INTO "locations" ("truck_id", "latitude", "longitude", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["truck_id", 1], ["latitude", 37.782267], ["longitude", -122.391248], ["created_at", "2019-10-10 04:12:12.953575"], ["updated_at", "2019-10-10 04:12:12.953575"]]
   (5.5ms)  commit transaction
=> #<Location id: 1, truck_id: 1, latitude: 0.37782267e2, longitude: -0.122391248e3, created_at: "2019-10-10 04:12:12", updated_at: "2019-10-10 04:12:12">
irb(main):004:0> truck.location
  Location Load (1.3ms)  SELECT  "locations"."latitude", "locations"."longitude" FROM "locations" WHERE "locations"."truck_id" = ? ORDER BY "locations"."created_at" DESC LIMIT ?  [["truck_id", 1], ["LIMIT", 1]]
=> [37.782267, 122.391248]
```

However, it broke down immediately when I tried to actually geocode anything:

```ruby
irb(main):005:0> truck.geocode
  Location Load (0.5ms)  SELECT  "locations"."latitude", "locations"."longitude" FROM "locations" WHERE "locations"."truck_id" = ? ORDER BY "locations"."created_at" DESC LIMIT ?  [["truck_id", 1], ["LIMIT", 1]]
Traceback (most recent call last):
        1: from (irb):5
NoMethodError (undefined method `latitude=' for #<Truck:0x00007ff3c2b58098>)
```

There's not really a good workaround for this error. Adding setter methods for `Truck#latitude=` and `Truck#longitude=` doesn't do the trick, because we don't want to create new `Location` records with only one of those attributes filled in. I thought about adding some kind of temp `Location` object that'd be persisted after both attributes were filled in, but at that point, the code smell was pretty obvious. It shouldn't be this difficult. I was clearly doing something wrong.

![Gob Bluth has made a huge mistake](https://www.freeutopia.org/wp-content/uploads/Ive-made-a-huge-mistake.jpg "Gob Bluth has made a huge mistake")


## What Worked 🏄‍♀️

I went back to the `geocoder` docs and recognized that `latitude` and `longitude` attributes had to live on the model I wanted to geocode (`Truck`). That's what the library expects, and things start to get gnarly if you try to override that.

Plus after giving it more thought, there's not much to be gained by separating out these properties into a separate table.

* There's no immediate need to keep track of past locations, so no need to make a premature optimization to support it.
* It's just as easy to drop columns as a table, should I want to deprecate this functionality in the future.

With that in mind, the next step was to undo the work I did to add `locations`, then run a migration to add `latitude` and `longitude` to `trucks`.

```ruby
# app/models/truck.rb
class Truck < ActiveRecord::Base
  geocoded_by :address
  after_validation :geocode

  def address
    nil # stubbed, not necessary for our requirements
  end

  def location
    [latitude, longitude]
  end

  def set_location(latitude:, longitude:)
    update(latitude: latitude, longitude: longitude)
  end
end

# schema.rb
ActiveRecord::Schema.define(version: 20191008351012) do
  create_table "trucks", force: :cascade do |t|
    t.string "name", null: false
    t.decimal "latitude", precision: 10, scale: 6, null: false
    t.decimal "longitude", precision: 10, scale: 6, null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
end
```

To the great suprise of no one, following the docs' recommended implementation allowed me to greatly simplify my code, and -- most importantly -- actually get it working.

```ruby
irb(main):006:0> truck.set_location(latitude: 37.782267, longitude: -122.391248)
   (0.1ms)  begin transaction
   (0.1ms)  commit transaction
=> true
irb(main):007:0> truck.geocoded?
=> true
```

Now that we can properly geocode our `Truck`s, we have access to helpful methods like `near` and `distance`:

```ruby
irb(main):008:0> nearby_trucks = Truck.near([truck.latitude + 0.1, truck.longitude + 0.1], 50)
  Truck Load (0.6ms)  SELECT  trucks.*, (69.09332411348201 * ABS(trucks.latitude - 37.882267) * 0.7071067811865475) + (59.836573914187355 * ABS(trucks.longitude - -122.29124800000001) * 0.7071067811865475) AS distance, CASE WHEN (trucks.latitude >= 37.882267 AND trucks.longitude >= -122.29124800000001) THEN  45.0 WHEN (trucks.latitude <  37.882267 AND trucks.longitude >= -122.29124800000001) THEN 135.0 WHEN (trucks.latitude <  37.882267 AND trucks.longitude <  -122.29124800000001) THEN 225.0 WHEN (trucks.latitude >= 37.882267 AND trucks.longitude <  -122.29124800000001) THEN 315.0 END AS bearing FROM "trucks" WHERE (trucks.latitude BETWEEN 37.15860808444576 AND 38.60592591555424 AND trucks.longitude BETWEEN -123.20811433750569 AND -121.37438166249433) ORDER BY distance ASC LIMIT ?  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Truck id: 1, name: "Maquette Fine Art Services", created_at: "2019-10-13 18:22:51", updated_at: "2019-10-13 18:23:55", latitude: 0.37782267e2, longitude: -0.122391248e3>]>
irb(main):008:0> nearby_trucks.first.distance
  Truck Load (1.2ms)  SELECT  trucks.*, (69.09332411348201 * ABS(trucks.latitude - 37.882267) * 0.7071067811865475) + (59.836573914187355 * ABS(trucks.longitude - -122.29124800000001) * 0.7071067811865475) AS distance, CASE WHEN (trucks.latitude >= 37.882267 AND trucks.longitude >= -122.29124800000001) THEN  45.0 WHEN (trucks.latitude <  37.882267 AND trucks.longitude >= -122.29124800000001) THEN 135.0 WHEN (trucks.latitude <  37.882267 AND trucks.longitude <  -122.29124800000001) THEN 225.0 WHEN (trucks.latitude >= 37.882267 AND trucks.longitude <  -122.29124800000001) THEN 315.0 END AS bearing FROM "trucks" WHERE (trucks.latitude BETWEEN 37.15860808444576 AND 38.60592591555424 AND trucks.longitude BETWEEN -123.20811433750569 AND -121.37438166249433) ORDER BY distance ASC LIMIT ?  [["LIMIT", 1]]
=> 9.116720519305336
```

## Takeaways

Working with the `geocoder` gem for the first time was a good reminder not to over-architect too early, and most importantly, go with what the docs recommend. Especially with an established, widely-used library like `geocoder`, you can trust the library authors to steer you in the right direction, especially for straight-forward use cases like mine. Let's hope future Kate remembers this next time she tackles a new library.

## References

- [Geocoder gem site](http://www.rubygeocoder.com/)
- [Geocoder gem source code](https://github.com/alexreisner/geocoder)