Category guides

Building a property search in Rails: geocoding gems compared

Category guides · · Published 2026-10-25

Coloured map pins stuck into a wooden world map, seen at table height with a shallow focus
Pins on a wooden map. Every listing in a location search starts as an address that has to become a point.

A property search looks like a filter form with a map on top, but underneath it does two separate jobs: turning addresses into coordinates (geocoding), and answering questions such as "what is within two kilometres of here?" (spatial querying). Rails has well-known gems for each, and the right combination depends on how central location is to your application.

Release figures are from RubyGems.org, October 2026.

The job

A typical case is an apartment search for buyers, where each listing needs coordinates so that a visitor can ask for flats for sale within two kilometres of a station and see them sorted by distance. That breaks down into four requirements:

  1. Geocode each address once, when it is created or changed, and store the result.
  2. Store the point in a column the database can index.
  3. Query by radius, combined with ordinary filters on price, rooms or surface, and sort by distance.
  4. Possibly query by shape: a polygon drawn on a map, a school catchment area, a district boundary.

The first three can be done with plain latitude and longitude columns. The fourth is where a spatial database earns its keep.

Geocoder

Geocoder (1.8.6, September 2025, MIT licence) is the most widely used option, with more than 113 million downloads on RubyGems.org. It wraps dozens of geocoding services behind one API, does reverse geocoding and IP lookups, and adds Active Record helpers: geocoded_by :address, an after_validation :geocode callback, and a near scope that returns records within a distance with distance and bearing attributes. By default it uses Nominatim for street addresses and ipinfo.io for IP addresses.

On PostgreSQL and MySQL, near first narrows candidates with a bounding box, so the README recommends a composite index on the latitude and longitude columns; on SQLite it falls back to a less accurate calculation because SQLite lacks trigonometric functions. It also works with MongoDB. Its only runtime dependencies are base64 and csv.

Use it when you need geocoding plus radius search on any of the common databases. It is also a good geocoding layer in front of PostGIS.

Geokit and geokit-rails

Geokit splits the work in two: the geokit gem geocodes and calculates distances (1.14.0, January 2023), and geokit-rails adds acts_as_mappable with distance scopes such as within and by_distance to Active Record models. Both are MIT-licensed. After more than three years without a release, geokit-rails shipped 2.5.1 and 2.5.2 on 22 October 2026. Its gemspec still declares rails >= 3.0 with no upper bound, which says nothing about which recent Rails versions are actually tested.

Use it when an existing application already depends on it. For new work Geocoder covers the same ground with a larger community. Our geokit-rails history has the details.

PostGIS, activerecord-postgis-adapter and RGeo

PostGIS is a PostgreSQL extension that adds geometry and geography types, spatial functions and GiST indexes. Three gems connect it to Rails:

Use it when location is central: polygons, many thousands of listings, nearest-neighbour queries or spatial joins. The cost is a PostgreSQL dependency and an extension to install on every environment, including CI.

Geocoding providers and their limits

The gem is free; the geocoding service behind it often is not, and every provider has rules.

Whatever the provider: geocode in a background job, never during a web request; store the coordinates; and geocode again only when the address changes.

Accuracy and edge cases

Geocoding results are not all equally precise. Depending on the provider and the address, a result may point to the building, to an interpolated position along the street, to the centre of a postcode or only to the town. Most services return a type or accuracy field with each result; store it next to the coordinates, so that a listing placed at the centre of a postcode is not presented as an exact location, and so you can find the weak results later.

New-build developments are a common gap: the street may not exist yet in OpenStreetMap or a commercial dataset, and the geocoder will quietly return the nearest match it knows. Let editors move the pin by hand and keep a flag that stops an automatic job from overwriting a corrected position. Finally, decide what the public map should show. Many property sites deliberately round or offset coordinates before display, keeping the precise point for internal queries only.

A working example: Geocoder plus PostGIS

This combination uses Geocoder only to obtain coordinates and PostGIS for storage and queries. The migration enables the extension and stores a geographic point, so distances are in metres:

# Gemfile
gem "geocoder"
gem "activerecord-postgis-adapter"   # database.yml: adapter: postgis

class CreateListings < ActiveRecord::Migration[8.1]
  def change
    enable_extension "postgis"
    create_table :listings do |t|
      t.string  :address, null: false
      t.integer :price_cents
      t.st_point :lonlat, geographic: true
      t.timestamps
    end
    add_index :listings, :lonlat, using: :gist
  end
end

Geocoder is configured to raise errors instead of logging them, so the job can retry:

# config/initializers/geocoder.rb
Geocoder.configure(
  lookup: :nominatim,
  timeout: 5,
  units: :km,
  always_raise: :all,
  http_headers: { "User-Agent" => "example-listings/1.0 (ops@example.com)" }
)

# app/jobs/geocode_listing_job.rb
class GeocodeListingJob < ApplicationJob
  retry_on Timeout::Error, Geocoder::OverQueryLimitError,
           wait: :polynomially_longer, attempts: 5

  FACTORY = RGeo::Geographic.spherical_factory(srid: 4326)

  def perform(listing)
    result = Geocoder.search(listing.address).first or return
    lat, lng = result.coordinates
    listing.update!(lonlat: FACTORY.point(lng, lat))
  end
end

The model enqueues the job when the address changes and exposes a radius scope. ST_DWithin on a geography column takes metres and uses the GiST index; the point is built with longitude first:

class Listing < ApplicationRecord
  after_commit -> { GeocodeListingJob.perform_later(self) },
               if: :saved_change_to_address?

  def self.within_metres(lat, lng, metres)
    origin = sanitize_sql_array(
      ["ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography", lng.to_f, lat.to_f]
    )
    where("ST_DWithin(lonlat, #{origin}, ?)", metres.to_f)
      .order(Arel.sql("ST_Distance(lonlat, #{origin})"))
  end
end

Listing.within_metres(48.8443, 2.3744, 2_000).where(price_cents: ..45_000_000)

With Nominatim's one-request-per-second limit, run geocoding jobs on a queue with a single worker, or throttle them; a backfill of 10,000 addresses takes close to three hours at that rate.

Which to choose

SituationSuggestion
Radius search on PostgreSQL, MySQL or SQLiteGeocoder with indexed latitude and longitude columns
Polygons, map-drawn areas, large datasetsPostGIS with activerecord-postgis-adapter and RGeo, Geocoder for geocoding
Existing application on geokit-railsKeep it, test it against your Rails version, plan a migration if it lags
Thousands of addresses to geocodeA paid or self-hosted geocoder, not the public Nominatim server

Location usually sits next to text filters and paging; see Search in Rails and Pagination in Rails for those parts of the page.