Building a property search in Rails: geocoding gems compared

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:
- Geocode each address once, when it is created or changed, and store the result.
- Store the point in a column the database can index.
- Query by radius, combined with ordinary filters on price, rooms or surface, and sort by distance.
- 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:
- activerecord-postgis-adapter (11.1.1, November 2025, BSD-3-Clause) replaces the
postgresqladapter withpostgisindatabase.ymland adds column types such asst_pointandst_polygon. Its major versions follow Active Record exactly: 11.1 requires Active Record 8.1, 11.0 targets 8.0, and the 10.0 series for Rails 7.2 received a patch release in April 2026. Upgrade it in the same pull request as Rails. - rgeo (3.1.0, January 2026, BSD-3-Clause) provides the Ruby objects for points, lines and polygons, and the spatial factories that define coordinate systems.
- rgeo-geojson (2.2.0, July 2024) reads and writes GeoJSON, the format most web map libraries expect.
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.
- Nominatim (OpenStreetMap data, Geocoder's default): the public server's usage policy sets an absolute maximum of one request per second, requires a User-Agent or Referer that identifies your application, asks bulk users to cache results, requires attribution and forbids autocomplete built on the API. For production volumes, self-host Nominatim or use a commercial provider built on OpenStreetMap.
- Google Geocoding API: according to Google's pricing list (checked October 2026), the Essentials SKU includes 10,000 free billable events a month, then costs $5.00 per 1,000 up to 100,000; the usage page gives a limit of 3,000 queries per minute. Read the terms on caching and on showing results on non-Google maps before you design around it.
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
| Situation | Suggestion |
|---|---|
| Radius search on PostgreSQL, MySQL or SQLite | Geocoder with indexed latitude and longitude columns |
| Polygons, map-drawn areas, large datasets | PostGIS with activerecord-postgis-adapter and RGeo, Geocoder for geocoding |
| Existing application on geokit-rails | Keep it, test it against your Rails version, plan a migration if it lags |
| Thousands of addresses to geocode | A 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.
Plugin historiesGeokit-rails
What geokit-rails and the Geokit gem do, their release status in 2026, and when to use Geocoder or PostGIS instead for geocoding and location queries in Rails.
Category guidesSearch
Filtering versus full-text search in Rails: Ransack for filter forms, pg_search for PostgreSQL full-text search, and Searchkick, Meilisearch or Thinking Sphinx for a dedicated search engine.
Category guidesBackground jobs
How to choose a background job backend for Rails: Active Job, Solid Queue (the Rails 8 default), Sidekiq, GoodJob, delayed_job and Resque, and what replaced Workling and Spawn.