_.--""--._ Nation ─┐ "Brasil" BR .br BRL +55
.' __ o '. │
/ .' \ \ Region ─┤ "São Paulo" SP America/Sao_Paulo
| / o | ~ | │
| \_ o / ~ | City ─┤ "São Paulo" sao-paulo-sp (-46.63, -23.55)
\ `-' ~ / │
'. ~ ~ ~ .' Hood ─┘ "Vila Madalena" sao-paulo-sp-vila-madalena
`--....--'
place.hood.city.region.nation.planet # => :earth
The whole planet as four Mongoid models. Nation → Region → City → Hood, with slugs that never collide, names in every script, geo queries, and a fallback chain for phone/postal codes. Bring your own data (Geonames, IBGE, a CSV, a hunch) — or let geonames_local fill it for you.
| gem | Nation | Region | City | Hood | DB | Geo | i18n names | Admin UI |
|---|---|---|---|---|---|---|---|---|
| countries | ✓ | ✓ | – | – | in-memory | – | ✓ | – |
| city-state | ✓ | ✓ | ✓ | – | in-memory | – | – | – |
| geonames-rails | ✓ | ✓ | ✓ | – | AR | ✓ | – | – |
| geonames_dump | ✓ | ✓ | ✓ | – | AR | ✓ | – | – |
| geopolitical | ✓ | ✓ | ✓ | ✓ | Mongoid | ✓ | ✓ | ✓ |
Static lists are great until a user wants their city in the dropdown, a neighborhood on the address, or a query like "cities within 50km". That's when you want documents, not constants.
gem 'geopolitical'Rails: it's an engine, models autoload. Optionally mount the admin UI:
# config/routes.rb
mount Geopolitical::Engine => '/geopolitical'The admin UI has no authentication of its own — it is a mountable engine, so guarding it is the host app's job. Either constrain the mount:
authenticate :user, ->(user) { user.admin? } do
mount Geopolitical::Engine => '/geopolitical'
endor hand the engine a controller of yours that already authenticates:
# config/initializers/geopolitical.rb
Geopolitical.parent_controller = 'Admin::BaseController'Plain Ruby: configure Mongoid, then require 'geopolitical'.
The engine ships en and pt (config/locales/geopolitical.*.yml) and follows
I18n.locale — model names, attribute labels, buttons and flashes all move together:
I18n.locale = :pt
City.model_name.human(count: 2) # => "Cidades"
Hood.human_attribute_name(:souls) # => "População"Override a string by defining the same key in your app, or add a locale by copying
one of those two files. Note the model data is localized separately — name and
alt are localize: true fields, so a city carries its own name per language.
br = Nation.create!(name: 'Brasil', abbr: 'br', tld: '.br', cash: 'BRL', langs: %w[pt], phone: '55')
sp = Region.create!(name: 'São Paulo', abbr: 'SP', nation: br, timezone: 'America/Sao_Paulo')
sampa = City.create!(name: 'São Paulo', region: sp, souls: 12_000_000, geom: [-46.63, -23.55])
vila = Hood.create!(name: 'Vila Madalena', city: sampa)
Nation['br'] # => #<Nation BR> abbr is the _id, any case
Nation.find('BR') # same thing
sampa.slug # => "sao-paulo-sp" region suffix, no collisions
vila.slug # => "sao-paulo-sp-vila-madalena"
sampa.nation # => BR derived from the region, you may omit it
sampa.to_s # => "São Paulo/SP"
sampa.with_nation('-') # => "São Paulo-SP-BR"
sampa.population # => 12000000 alias of `souls`
vila.phone # => "55" hood → city → region → nation
br.currency # => "BRL" alias of `cash`
City.search('sao pa') # slug prefix, accent/case-insensitive
City.search('sao-paulo-sp', exact: true)
City.nearby(sampa.geom) # 2dsphere $near, needs City.create_indexes
City.population.first # biggest city
Region.ordered # by nameTwo ways in, one answer out: a Found with the street, number, hood, postal
code and the City (found, or made under its state's Region).
typing "av paulista 1578" ──suggest──► [Suggestion(ref, text), …] free, nothing kept
the person picks one ──pick─────► Found one call, yours to store
a Brazilian CEP ──find─────► Found free, no key
# config/initializers/geopolitical.rb
Geopolitical.postal_provider = :google # :google · :geoapify — nil means no typing search
Geopolitical.postal_key = ENV['GOOGLE_MAPS_KEY']
token = SecureRandom.uuid # one per form: Google bills the typing as one session
Geopolitical::Postal.suggest('av paulista 1578', session: token, nation: 'BR')
# => [#<data Suggestion ref="ChIJ…" text="Avenida Paulista, 1578 - Bela Vista, São Paulo - SP, Brasil">]
found = Geopolitical::Postal.pick('ChIJ…', session: token)
found.street # => "Avenida Paulista"
found.number # => "1578"
found.city # => #<City São Paulo/SP>
found.ref # => "ChIJ…" the provider's id, the one thing Google lets you keep forever
Geopolitical::Postal.find('01311-925') # a CEP, masked or not — BrasilAPI, whatever the provider
Geopolitical::Postal.find('10001', nation: 'US') # => nil — no guessing| provider | data | store the pick? | free |
|---|---|---|---|
:google |
best, Brazil too | yes — the address the person picked (Places policy) | typing unlimited in a session, 10k picks/mo |
:geoapify |
OpenStreetMap | yes | 3,000 requests/day |
| BrasilAPI | Correios (CEP) | yes | no key, find only |
Google's pick asks the Essentials fields and nothing else
(Postal::Google::FIELDS) — one field more and it bills as Pro. Suggestions
and picks are never cached; a CEP is cached a day where Rails has a cache.
Offline, unknown, a bad key: [] or nil, and the form stays open for a
person to type.
Slugs are the API. They're stable, unique, URL-safe and they survive scripts you can't transliterate:
"São Paulo" => "sao-paulo"
"Baden-Württemberg"=> "baden-wurttemberg"
"Hà Nội" => "ha-noi"
"St. Louis" => "st-louis"
"東京" => "東京"
"Санкт-Петербург" => "санкт-петербург"
"دبي" => "دبي"
Two Springfields? springfield-il and springfield-ma. Tokyo with a region
that has no abbr? tokyo-東京都. It just works.
Nation _id = abbr (ISO 3166-1 α2) gid tld cash code3 langs capital
└── Region abbr (ISO 3166-2) code timezone capital unique per nation
└── City geom (Point) area rbbr slug unique globally
└── Hood rank slug unique globally
Every model shares (via the Geopolitocracy concern):
name & alt (localized), abbr, nick, ascii, code, slug,
souls/population, postal, phone, .search, .ordered, to_s, ==, <=>.
Names get titleized only when you shout or mumble: 'new york' and
'NEW YORK' become "New York"; 'McAllen' stays "McAllen".
geonames_local downloads Geonames dumps and writes straight into these models:
geonames BR -c geonames.yml # all of Brasil: regions, cities, hoods
Or hand-roll from any source — every model is just a Mongoid document.
bundle install
bundle exec rspec # needs a local mongod
Specs take a world tour (spec/models/world_spec.rb): Japan, Russia, Greece,
Egypt, India, Korea, Germany, the US, Singapore… add your country if it's
missing, that's the best PR you can send.
MIT. Bug reports and PRs at fireho/geopolitical.