A Ruby gem for working with Japanese address data -- prefectures, cities, postal codes, and regions -- in a unified interface.
Dealing with Japanese addresses is tedious.
- Looking up an address from a postal code requires parsing CSVs and loading them into a database
- Maintaining master data for prefectures and cities means writing migrations
- Postal code auto-fill and prefecture-city cascading selects end up being rewritten every project
- Existing gems are tightly coupled to Rails, have outdated data, or lack Hotwire support
Basho solves all of these.
- No DB migrations -- All data is bundled as JSON. Just
gem installand go - Optional DB backend -- Generate tables for JOINs and foreign keys. The API auto-switches transparently
- Framework-agnostic -- Works with plain Ruby, Sinatra, Rails API-only, or any Ruby app
- ActiveRecord integration --
include Basho+ a one-line macro for automatic postal code to address resolution on save - Hotwire-ready -- Built-in Rails Engine with postal code auto-fill via Turbo Frame + Stimulus
- Lightweight -- Immutable models via
Data.define, lazy loading, zero external dependencies
- Ruby 3.2 / 3.3 / 3.4 / 4.0
# Gemfile
gem "basho"bundle installpostal = Basho::PostalCode.find("154-0011")
postal.prefecture_name # => "東京都"
postal.city_name # => "世田谷区"
postal.town # => "上馬"class User < ApplicationRecord
include Basho
basho_postal :postal_code,
prefecture: :pref_name,
city: :city_name,
town: :town_name
end
user = User.new(postal_code: "154-0011")
user.save
user.pref_name # => "東京都"
user.city_name # => "世田谷区"
user.town_name # => "上馬"Basho::Prefecture.find(13).name # => "東京都"
Basho::Prefecture.where(region: "関東") # => 7 prefectures
Basho::City.find("131016").name # => "千代田区"# Class methods
Basho::Prefecture.find(13) # Find by code (Integer)
Basho::Prefecture.find(name: "東京都") # Find by Japanese name
Basho::Prefecture.find(name_en: "Tokyo") # Find by English name
Basho::Prefecture.all # All 47 prefectures
Basho::Prefecture.where(region: "関東") # Filter by region name# Instance methods / members
pref = Basho::Prefecture.find(13)
pref.code # => 13 (Integer)
pref.name # => "東京都" (String)
pref.name_en # => "Tokyo" (String)
pref.name_kana # => "トウキョウト" (String, katakana)
pref.name_hiragana # => "とうきょうと" (String, hiragana)
pref.region_name # => "関東" (String)
pref.type # => "都" (String: "都" / "道" / "府" / "県")
pref.capital_code # => "131016" (String, 6-digit city code)
pref.region # => Basho::Region
pref.cities # => Array<Basho::City>
pref.capital # => Basho::City (prefectural capital)# Class methods
Basho::City.find("131016") # Find by 6-digit municipality code (String)
Basho::City.where(prefecture_code: 13) # Filter by prefecture code (Integer)
Basho::City.valid_code?("131016") # Validate JIS X 0401 check digit# Instance methods / members
city = Basho::City.find("131016")
city.code # => "131016" (String, 6-digit)
city.prefecture_code # => 13 (Integer)
city.name # => "千代田区" (String)
city.name_kana # => "チヨダク" (String, katakana)
city.district # => nil (String or nil, e.g. "島尻郡")
city.capital # => false (Boolean, raw member)
city.capital? # => false (Boolean, prefectural capital?)
city.full_name # => "千代田区" (String, prepends district if present)
city.prefecture # => Basho::Prefecturedistrict is set only for towns/villages that belong to a county (gun). For example:
city = Basho::City.find("473821")
city.name # => "八重瀬町"
city.district # => "島尻郡"
city.full_name # => "島尻郡八重瀬町"find returns a single PostalCode or nil. where returns an Array (may contain multiple results for shared postal codes).
# Class methods
Basho::PostalCode.find("154-0011") # => PostalCode or nil (first match)
Basho::PostalCode.find("1540011") # Hyphenless format also works
Basho::PostalCode.where(code: "154-0011") # => Array<PostalCode># Instance methods / members
postal = Basho::PostalCode.find("154-0011")
postal.code # => "1540011" (String, 7 digits, no hyphen)
postal.formatted_code # => "154-0011" (String, with hyphen)
postal.prefecture_code # => 13 (Integer)
postal.city_name # => "世田谷区" (String)
postal.town # => "上馬" (String)
postal.prefecture_name # => "東京都" (String)
postal.prefecture # => Basho::Prefecture9 regions: Hokkaido, Tohoku, Kanto, Chubu, Kinki, Chugoku, Shikoku, Kyushu, Okinawa.
# Class methods
Basho::Region.all # => Array of 9 regions
Basho::Region.find("関東") # Find by Japanese name
Basho::Region.find("Kanto") # Find by English name# Instance methods / members
region = Basho::Region.find("関東")
region.name # => "関東" (String)
region.name_en # => "Kanto" (String)
region.prefecture_codes # => [8, 9, 10, 11, 12, 13, 14] (Array<Integer>)
region.prefectures # => Array<Basho::Prefecture>Add include Basho to your model to enable the basho and basho_postal macros.
class Shop < ApplicationRecord
include Basho
basho :local_gov_code
end
shop.city # => Basho::City
shop.prefecture # => Basho::Prefecture
shop.full_address # => "東京都千代田区"basho :column defines three instance methods and a scope:
| Method | Return value |
|---|---|
city |
Basho::City found by the column value |
prefecture |
Basho::Prefecture via city.prefecture |
full_address |
"#{prefecture.name}#{city.name}" or nil |
with_basho |
Scope that preloads city and prefecture (N+1 prevention) |
Use the with_basho scope when loading multiple records that access city or prefecture:
# Without: N+1 queries (1 + N×2)
Shop.all.each { |s| s.full_address }
# With: 3 queries total
Shop.with_basho.each { |s| s.full_address }with_basho works in both memory and DB mode. In memory mode it is a no-op; in DB mode it eager-loads the associations. This means you can add it before switching to DB mode -- no code changes needed later.
class Shop < ApplicationRecord
include Basho
basho_postal :postal_code
end
shop.postal_address # => "東京都世田谷区上馬"basho_postal :column (without mapping options) defines a postal_address method that returns "#{prefecture_name}#{city_name}#{town}" or nil.
When you pass mapping options to basho_postal, it registers a before_save callback that auto-fills address columns whenever the postal code column changes.
class User < ApplicationRecord
include Basho
basho_postal :postal_code,
prefecture: :pref_name,
city: :city_name,
town: :town_name
endAvailable mapping keys:
| Key | Resolved value |
|---|---|
prefecture: |
Prefecture name (e.g. "東京都") |
city: |
City name (e.g. "世田谷区") |
town: |
Town name (e.g. "上馬") |
prefecture_code: |
Prefecture code (e.g. 13) |
city_code: |
6-digit municipality code (e.g. "131130") |
- Resolution runs only when the postal code column will change on save
- Partial mappings are supported (e.g.
prefecture:only) - The
postal_addressmethod is always defined regardless of mapping options
A built-in Rails Engine providing postal code auto-fill via Turbo Frame + Stimulus.
Mount the Engine in your routes:
# config/routes.rb
mount Basho::Engine, at: "/basho"The Engine provides a single route:
| Method | Path | Controller#Action |
|---|---|---|
| GET | /basho/postal_codes/lookup?code=1540011 |
Basho::PostalCodesController#lookup |
The Stimulus controller and form helper are automatically registered via importmap and ActionView initializers.
Automatically fills in prefecture, city, and town fields when a 7-digit postal code is entered.
<%= form_with(model: @shop) do |f| %>
<div data-controller="basho--auto-fill"
data-basho--auto-fill-url-value="<%= basho.postal_code_lookup_path %>">
<%= f.text_field :postal_code,
data: { action: "input->basho--auto-fill#lookup",
"basho--auto-fill-target": "input" } %>
<turbo-frame id="basho-result"
data-basho--auto-fill-target="frame"
data-action="turbo:frame-load->basho--auto-fill#fill"></turbo-frame>
<div data-basho--auto-fill-target="fields" hidden>
<%= f.text_field :prefecture, disabled: true,
data: { "basho--auto-fill-target": "prefecture" } %>
<%= f.text_field :city, disabled: true,
data: { "basho--auto-fill-target": "city" } %>
<%= f.text_field :town, disabled: true,
data: { "basho--auto-fill-target": "town" } %>
</div>
</div>
<% end %>How it works:
- User enters a 7-digit postal code
- A Turbo Frame request is sent to the Engine's lookup endpoint
- Stimulus fills the prefecture, city, and town fields and shows the
fieldscontainer - When the postal code is cleared or incomplete, the fields are hidden
The fields target is optional. Without it, the fields remain visible and are simply cleared.
You can use the basho_autofill_frame_tag helper instead of writing the <turbo-frame> tag manually:
<%= basho_autofill_frame_tag %>This renders:
<turbo-frame id="basho-result"
data-basho--auto-fill-target="frame"
data-action="turbo:frame-load->basho--auto-fill#fill"></turbo-frame>| Type | Name | Description |
|---|---|---|
| Value | url (String) |
Lookup endpoint URL (required) |
| Target | input |
Postal code input field |
| Target | frame |
Turbo Frame for server response |
| Target | prefecture |
Prefecture output field |
| Target | city |
City output field |
| Target | town |
Town output field |
| Target | fields |
Container to show/hide (optional) |
Basho provides the data -- build the UI in your app using Turbo Frame.
# app/controllers/cities_controller.rb
class CitiesController < ApplicationController
def index
@cities = Basho::City.where(prefecture_code: params[:prefecture_code].to_i)
end
end<%# app/views/cities/index.html.erb %>
<turbo-frame id="city-select">
<%= f.select :city_code,
@cities.map { |c| [c.name, c.code] },
{ include_blank: "Select city" } %>
</turbo-frame><%# In your form %>
<%= f.select :prefecture_code,
Basho::Prefecture.all.map { |p| [p.name, p.code] },
{ include_blank: "Select prefecture" },
data: { action: "change->auto-submit#submit",
turbo_frame: "city-select" } %>
<turbo-frame id="city-select">
<%= f.select :city_code, [], include_blank: "Select city" %>
</turbo-frame>This keeps the HTML and styling in your app where it belongs.
The data API and ActiveRecord integration work without Hotwire. If you don't mount the Engine, no routes or Stimulus controllers are loaded.
# Just the data API -- works in any Ruby app
require "basho"
Basho::PostalCode.find("154-0011").town # => "上馬"
Basho::Prefecture.find(13).name # => "東京都"
Basho::City.where(prefecture_code: 13) # => Array<City># ActiveRecord integration -- works in any Rails app, Hotwire or not
class Shop < ApplicationRecord
include Basho
basho_postal :postal_code, city_code: :city_code, town: :town
endBy default, Basho loads all data from bundled JSON files -- no database needed. If you need JOINs, foreign key constraints, or want to reference basho_prefectures / basho_cities from your own tables, you can optionally generate DB tables.
rails generate basho:install_tables
rails db:migrate
rails basho:seedThis creates two tables:
| Table | Primary Key | Rows |
|---|---|---|
basho_prefectures |
code (integer) |
47 |
basho_cities |
code (string, 6-digit) |
~1,900 |
Once the tables exist, the public API (Basho::Prefecture.find, Basho::City.where, etc.) automatically uses the DB backend. No code changes required.
# Works exactly the same whether DB tables exist or not
Basho::Prefecture.find(13).name # => "東京都"
Basho::City.find("131016").full_name # => "千代田区"
Basho::City.where(prefecture_code: 13) # => ArrayDetection happens once on first access via Basho.db? and is cached for the process lifetime.
# Your app migration
add_foreign_key :shops, :basho_cities, column: :city_code, primary_key: :code
add_foreign_key :shops, :basho_prefectures, column: :prefecture_code, primary_key: :codeWhen you need ActiveRecord features (JOINs, scopes, eager loading), use the DB models directly:
# JOINs
Basho::DB::City.joins(:prefecture).where(basho_prefectures: { region_name: "関東" })
# Eager loading
Basho::DB::Prefecture.includes(:cities).find(13)
# Associations
prefecture = Basho::DB::Prefecture.find(13)
prefecture.cities # => ActiveRecord::Relation
prefecture.capital # => Basho::DB::Citybasho:seed is idempotent. Run it again after updating the gem to refresh the data:
rails basho:seed- Postal codes are not included in the DB tables (120k+ rows, updated monthly). They are always served from bundled JSON files.
Basho.db?is thread-safe and cached. UseBasho.reset_db_cache!if you need to re-detect (e.g. after running migrations in a test).
| Data | Source | Update Frequency |
|---|---|---|
| Prefectures | Ministry of Internal Affairs, JIS X 0401 | Rarely changes |
| Cities | Ministry of Internal Affairs, Local Government Codes | A few times per year |
| Postal codes | Japan Post KEN_ALL.csv | Monthly (auto-updated via GitHub Actions) |
| Regions | 9 regions (hardcoded) | Never changes |
git clone https://github.com/wagai/basho.git
cd basho
bin/setup
bundle exec rspecMIT License