郵便番号から住所を自動入力する機能は、住所フォームではほぼ必ず目にしますが、実際に作るとかなり面倒なことがいくつかあります。
例えば、郵便番号と住所の対応情報(KEN_ALL.CSV)は日本郵便が提供していますが、そのフォーマットが特殊なことがまず挙げられます。
また、対応情報は毎月更新されており、それを反映しないと自動入力できない郵便番号が増えていきます。一度パースすればそれで終了とはならないのです。

jipcodeの設計

jipcodeの設計は、対応情報の取り込みスクリプトと検索モジュールで構成されています。

取り込みスクリプトは対応情報を検索しやすいデータに変換して保存し、CIで毎月自動で実行されるようになっています。
そのため、jipcodeの郵便番号と住所の対応情報はGithub上では最新を保てるようになっています。(ただし、rubygemsへのリリース作業は目視確認のため手動で行っています。)

検索モジュールは郵便番号で保存しているデータから住所を検索し、結果を出力します。
結果は都道府県、市区町村、番地などを含む住所情報のHashの配列です。なぜ配列かというと一つの郵便番号に複数の住所が対応していることがあるためです。

この記事では取り込みスクリプトについて紹介したいので、検索モジュールの詳細は割愛します。とはいえ検索モジュールはバージョン2.0.0では43行の短いコードで、jipcodeの通常の使用ではそれしか使わないので気になったら流し見してみても良いかもしれません。
https://github.com/rinkei/jipcode/blob/master/lib/jipcode.rb

取り込みスクリプト

取り込みスクリプトは次のような構成です。

  • 対応情報のダウンロード
    • (一般の)郵便番号データ
    • 事業所の個別郵便番号データ
  • 対応情報の取り込み
    • 解凍・文字コード変換
    • パース
    • 検索しやすいデータにして保存

ダウンロードする対応情報は、一般的な住所に対する郵便番号データだけでなく、配達物数の多い大口事業所を表す個別の郵便番号データも存在します。これらのデータのフォーマット(列の位置)は異なっており、取り込み時に考慮する必要があります。

対応情報の取り込みでは、まずzipを解凍した後で文字コードをSHIFT JISからUTF-8に変換しています。この変換はnkfを使っています。

パース

解凍・文字コード変換後、パースに入ります。元のフォーマットはCSVですが、KEN_ALL.CSVのCSVは単純なコンマで区切られたデータではありません。特徴的な仕様として、次のようなものがあります。

全角となっている町域名の文字数が38文字を超える場合、また、半角カタカナとなっている町域名のフリガナが76文字を越える場合には、複数レコードに分割しています。

郵便番号データの説明 - 日本郵便 より

バカな仕様ですが、仕方がないので合わせてみましょう。
ところで、町域が38文字の行と、38文字を超えて複数行になった行をどのように区別したら良いでしょうか。わかりません。
とりあえず38文字の行が何件あるか気になりますね。2020年4月の郵便番号データから抽出したところ5件でした。

茂田井(1〜500「211番地を除く」「古町」、2527〜2529「土遠」)
塩屋町(三条通西洞院西入、三条通小川東入、三条通小川西入、三条通油小路東入)
百足屋町(夷川通柳馬場西入、夷川通堺町東入、夷川通堺町西入、夷川通高倉東入)
橘町(下珠数屋町通不明門東入、下珠数屋町通烏丸東入、下珠数屋町通東洞院西入)
骨屋町(諏訪町通高辻上る、諏訪町通高辻下る、高辻通烏丸西入、高辻通室町東入)

「…郵便番号は12万件以上あるのに町域が38文字以上の住所が5件だけ?」
この5件に共通するのは に囲まれた補足情報が含まれている点です。考えてみれば町名の長さだけで38文字を超えることはあり得ない気がします。そうすると町域が38文字を超えて行が分割される場合、この補足情報が長い可能性があります。それにより分割される場合、 で始まって で終わらないかもしれません。この推測は当たりました。そういった行を探してみたところ次のような町域が200件ほど見つかりました。

協和(88−2、271−10、343−2、404−1、427−
札内町(5、9、11−12、36、42−2、62、80、95、
川下(782−13、5363−7〜8、5382−3、5405

これらは38文字ではありません。また同様に を含まず で終わる行を探すと同じ数だけ見つかります。ただし、それらの行は連続しておらず、間に も含まない行が存在する場合もあります。パースではこういった行をまたいだデータに対応しなければなりません。

また町域の中には、 以下に掲載がない場合の次に番地がくる場合 といった説明の文字列も含まれていることがあります。それらは有効な町域が含まれていることもあるので、適切に抽出します。

このようにデータから都道府県、市区町村、町名を取得して、それらを郵便番号の上3桁ごとに別ファイルに保存します。検索モジュールでは与えられた郵便番号の上3桁からこのファイルを取得し、郵便番号が一致するデータが出力になります。

CSVは公式の仕様がない

CSVのRFCにはこう書かれています。

Due to lack of a single specification, there are considerable differences among implementations.
Implementors should "be conservative in what you do, be liberal in what you accept from others" (RFC 793 [8]) when processing CSV files.

「CSVは単一の仕様がないため、実装によって違いがある。実装する際は保守的に、実装を受け入れる際は寛容に。」という指針を示しています(引用は通信における設計原則・堅牢性原則)。

それにしたって行をまたぐなよとは思いますが、それはそれとして、おかげさまでjipcodeは2020/03/25に2周年を迎えました。
これまでに都道府県コードやString#match?への対応などのPRをいただけて、貴重なOSS開発ができているのは個人的にはありがたかったります。
とはいえ目標はリリースの完全自動化なので、今後とも頑張っていきます💪