郵便番号データのつくりかた

[OPENLOGI AdventCalendar](https://qiita.com/advent-calendar/2018/openlogi) 20日目担当の細川です。

先日 [FizzBuzz 問題どや顔で解くひとなんかよりも "KEN_ALL.csv" をうまく扱える人の方が社会的貢献度高い](https://togetter.com/li/352382) を見かけまして、自分も社会貢献してみたいな、と思いました。

郵便番号データは多くのサービスにとって最も基礎的なマスタデータですが、その作成は闇に包まれていると聞きました。ふと興味を持った~~のが間違いでした~~ので自作にトライしてみました。

作成したソースは[こちら](https://github.com/yhosok/postcode)にあります

## 準備

生成元データは日本郵便の公式サイトにある[読み仮名の促音・拗音を小書きで表記するもの](https://www.post.japanpost.jp/zipcode/dl/kogaki-zip.html)を使います。ここからあの有名な`KEN_ALL.CSV`が入手できます。

このファイルは以下に記載しますようにそのままではシステム的な用途では使いにくいため、使いやすい形に変換する必要があります。かなり手強いという評判ですから、変換用プログラムに使う言語には切れ味に定評のある[Haskell](https://www.haskell.org/)を使います。

Haskell開発環境としては、 [vscode と haskell-ide-engine で Haskell 開発環境を構築する](https://qiita.com/waddlaw/items/b83cd10311200095fe87) などを参考にさせていただき、 [stack](https://www.haskellstack.org/) & [haskell-ide-engine](https://github.com/haskell/haskell-ide-engine) に vscode をつないだものを利用しました。私はHaskell普段はあまり触らないのですが、快適な環境が簡単に手に入り感動です。

## 目標

変換後の郵便番号データの仕上がり具合を判定する材料として、無料で変換済みのものを配布されている[zipcloudさん](http://zipcloud.ibsnet.co.jp/)のものを参考にしました。こちらでは[apiサービス](http://zipcloud.ibsnet.co.jp/doc/api)も最新のデータで提供されていて素晴らしいです。

## つくりかた

### 元ファイルの文字コードを変えておく
日本郵便サイトから取得した `KEN_ALL.CSV`は文字コードがShift-JISなのですが、Haskellでやる上で色々と面倒な予感がするため、UTF-8にしておきます。スマートにそのまま変換したいところですが、今回の趣旨の中心的なものではないので端折りました。すみません。

### `KEN_ALL.CSV`の問題点を知る

[zipcloudさんのサイト](http://zipcloud.ibsnet.co.jp/)に`KEN_ALL.CSV`からの変換点として下記の記載があります。

- 町域名が「以下に掲載がない場合」の場合は、ブランク（空文字）に置換
- 町域名が「○○市（または町・村）の次に番地がくる場合」の場合は、ブランク（空文字）に置換
- 町域名が「○○市（または町・村）一円」の場合は、ブランク（空文字）に置換
- 町域名で、丸括弧で囲まれている部分を除去

つまり、このファイルの変換で問題になるのは`町域名`という、CSVの9番目の項目（半角カナは6番目）の取扱だけ、ということになります。

上の変換項目だけ眺めると、単純にパターンマッチして置換すれば良さそうに見えますが、最後の`町域名で、丸括弧で囲まれている部分を除去`には注意書きがありまして、

```
※町域名の文字数が多いために複数行に分割されてしまっている場合は、1行にマージしておいてから丸括弧を除去します。
※括弧内の文字が「地名」や「ビルの階」など、住所として使えそうなものは町域名の末尾に追加します。
※括弧内に地名が複数列挙されている場合は複数行に分割します。
```

とのこと。実際に見てみると、町域名だけが下記のように複数行にまたがっているものが存在します。

```
"協和（８８－２、２７１－１０、３４３－２、４０４－１、４２７－"
"３、４３１－１２、４４３－６、６０８－２、６４１－８、８１４、８４２－"
"５、１１３７－３、１３９２、１６５７、１７５２番地）"
```

つまり、

- このCSVファイルは1行が1データとは限らないだけではなくて、1行のCSVレコードとして完結すらしてない場合がある
- 1行のCSVレコードは`町域名`に括弧が無い、あるいは、両括弧が揃うまで町域名をマージした複数行によって得られる
- `町域名`括弧内の文字列は複数の町域名を表現している場合があり、その場合は更に分割された町域名ごとに同一郵便番号の複数レコードを生成する必要がある

というCSVデータとしては結構ダイナミックな取扱が必要ということになります。ここを制覇できるかどうか、つまりいかに`町域名`を取り扱うかが`KEN_ALL.CSV`の問題点と言えそうです。

### CSVレコードを読み込む

今回HaskellでのCSVパーサーには[cassava](https://hackage.haskell.org/package/cassava-0.5.1.0)を使ってみました。丸っとcsvレコードをレコード形式のカスタム型への変換するならば、data宣言に`deriving (Generic)`した上で`FromRecord`や`ToRecord`のinstance宣言だけすれば、基本的な型の値については変換の実装不要になります。もちろん個別実装も可能です。また、今回は使いませんでしたが、`FromNamedRecord`と`ToNamedRecord`のinstance実装することで、ヘッダーの名称からencode & decodeすることもできるようです。

今回は適当なフィールド名で全て取り込むことにします。

```haskell:Lib.hs
{-# LANGUAGE DeriveGeneric #-}

import           Data.Csv
import           GHC.Generics                   ( Generic )

data Postcode = Postcode
    { jis :: !String
    , postcode5 :: !String
    , postcode :: !String
    , prefectureKana :: !String
    , cityKana :: !String
    , townAreaKana :: !String
    , prefecture :: !String
    , city :: !String
    , townArea :: !String
    , isOneTownByMultiPostcode :: !Int
    , isNeedSmallAreaAddress :: !Int
    , isChome :: !Int
    , isMultiTownByOnePostcode :: !Int
    , updated :: !Int
    , updateReason :: !Int
    }
    deriving (Generic, Show, Eq)

instance FromRecord Postcode
instance ToRecord Postcode
```

今回は読み込み->変換->CSV出力をしてみたいと思いますので、メインの処理は下記のようになりました。`V.foldM_ (convert' output) (Nothing, Nothing) records` のところが中心となる変換処理になります。fold結果の型は`(町名域文字列の括弧が閉じてない前のデータ, 郵便番号変更あった行のデータ)`で、出力データの判定に使用します。

```haskell:Lib.hs
import qualified Data.ByteString.Lazy          as BL
import qualified Data.Vector                   as V
import           System.IO

convert :: FilePath -> FilePath -> IO ()
convert input output = do
    csvData <- BL.readFile input
    BL.writeFile output BL.empty
    case decode NoHeader csvData of
        Left  err     -> putStrLn err
        Right records -> V.foldM_ (convert' output) (Nothing, Nothing) records
```

### 町域名に基づいたCSVレコードのマージ

ここからが本丸です。まずは複数行にまたがった町域名をマージして、CSVレコードとして完結したものにしてから、各種置換などの処理を行う必要があります。下記はparseされたcsvをレコードごとにfoldする関数になりますが、`町域名(townArea)`を見て括弧が開いて閉じてない状態の場合は次の畳込み（fold）に値を回し、次行とマージ、括弧が閉じてからencodeして出力します。

出力の際には、変換後の値が前回出力行と重複するケースがあるため、比較してフィルタをするようにしています。

ちなみにHaskell上での正規表現関連は今回 [regex-tdfa](http://hackage.haskell.org/package/regex-tdfa) と [regex-compat-tdfa](http://hackage.haskell.org/package/regex-compat-tdfa) を使ってみました。Unicodeをサポートしています。

```haskell
regexUnClosedParentheses :: String
regexUnClosedParentheses = "（.*[^）]$"

regexUnClosedParenthesesKana :: String
regexUnClosedParenthesesKana = "\\(.*[^\\)]$"

convert'
    :: FilePath
    -> (Maybe Postcode, Maybe Postcode)
    -> Postcode
    -> IO (Maybe Postcode, Maybe Postcode)
convert' output (lastUnClosed, lastChanged) current 
    | isUnClosed target = return (Just target, lastChanged)
    | otherwise = write targetToWrite >> return (Nothing, nextLastChanged)
  where
    target = maybe current (const merged) lastUnClosed
    merged = current
        { townArea = (townArea . fromJust $ lastUnClosed) ++ townArea current
        , townAreaKana = (townAreaKana . fromJust $ lastUnClosed)
                             ++ nextUnClosedKana
        }
    targetToWrite = filter isNotDup $ postCodeWithTownArea target
    isNotDup p = maybe True (not . isSameAddress p) lastChanged
    nextUnClosedKana
        | isUnClosedKana . fromJust $ lastUnClosed = townAreaKana current
        | otherwise                               = ""
    isUnClosed p = townArea p =~ regexUnClosedParentheses :: Bool
    isUnClosedKana p = townAreaKana p =~ regexUnClosedParenthesesKana :: Bool
    write = BL.appendFile output . encode
    isSamePostcode p =
        maybe False (\lp -> postcode lp == postcode p) lastChanged
    nextLastChanged | null targetToWrite = lastChanged
                    | isSamePostcode $ head targetToWrite = lastChanged
                    | otherwise = Just $ head targetToWrite

isSameAddress :: Postcode -> Postcode -> Bool
isSameAddress p p' = all (\f -> f p == f p') fields
  where
    fields =
        [ postcode
        , prefectureKana
        , cityKana
        , townAreaKana
        , prefecture
        , city
        , townArea
        ]

```

### 変換処理

次は町域名に従ったレコードごとの変換処理をします。町域名の変換後に、複数の町域名が生成される可能性があるので、その数分だけ町域名違いのレコードデータを生成します。すぐ後に記載しますが、町域名に何か書いてあっても町域名ではない場合があるので、その場合は町域名空文字にレコードを生成します。

```haskell
postCodeWithTownArea :: Postcode -> [Postcode]
postCodeWithTownArea p =
    map (\(ta, taKana) -> p { townArea = ta, townAreaKana = taKana })
        $ convertTownArea p
```

### 町域名変換
#### 町域名無し

まずは町域名が無しと考えるべきケース。これはzipcloudさんの解説を参考にしてますが、`以下に掲載がない場合`, `〜(市|町|村)の次に番地がくる場合`, `〜(市|町|村)一円` がマッチする場合には町域名無しと考えます。

```haskell
regexIgnore :: String
regexIgnore = "以下に掲載がない場合|(市|町|村)の次に番地がくる場合|(市|町|村)一円"

convertTownArea :: Postcode -> [(String, String)]
convertTownArea p
    | ta =~ regexIgnore
    = [("", "")]
...
```

#### 括弧の中がビルの階層

`中央アエル（１階）`といった場合は、括弧を取ってあげます。

```haskell
regexFloor :: String
regexFloor = "（([０-９]+階)）"

regexFloorKana :: String
regexFloorKana = "\\(([0-9]+ｶｲ)\\)"

convertTownArea :: Postcode -> [(String, String)]
convertTownArea p
...
    | ta =~ regexFloor
    = [(replace regexFloor ta "　\\1", replace regexFloorKana taKana "\\1")]
...
```

#### `地割`の対応

岩手県に特有らしいのですが、`地割り`という表記があり、`穴明２２地割、穴明２３地割`や`葛巻（第４０地割「５７番地１２５、１７６を除く」～第４５地割）`という町域名が現れる場合があります。これをそれぞれ`穴明`、`葛巻`に変換します。

```haskell
regexJiwari :: String
regexJiwari = "^([^０-９第（]+)[第]*[０-９]+地割.*"

regexJiwariKana :: String
regexJiwariKana = "^([^0-9\\(]+)(ﾀﾞｲ)*[0-9]*ﾁﾜﾘ.*"

convertTownArea :: Postcode -> [(String, String)]
convertTownArea p
...
    | ta =~ regexJiwari
    = [(replace regexJiwari ta "\\1", replace regexJiwariKana taKana "\\1")]
...
```

#### 括弧内の変換

町域名に括弧が存在し、上記のビル階層などでは無い場合は括弧内文字列の変換をします。`大通西（２０～２８丁目）`のような場合は`大通西`に、`留萌村（峠下）`のような場合は`留萌村`と`留萌村峠下`に分割します。詳細については後ほど記載します。

```haskell
regexParentheses :: String
regexParentheses = "（(.*)）"

regexParenthesesKana :: String
regexParenthesesKana = "\\((.*)\\)"

convertTownArea :: Postcode -> [(String, String)]
convertTownArea p
...
    | ta =~ regexParentheses
    = [(replaceParentheses "", replaceParenthesesKana "")] ++ converAndReplace p
...
  where
    ta               = townArea p
    taKana           = townAreaKana p
    converAndReplace = map replaceParentheses' . convertInParentheses
    replaceParentheses' (ta', taKana') =
        (replaceParentheses ta', replaceParenthesesKana taKana')
    replaceParentheses     = replace regexParentheses ta
    replaceParenthesesKana = replace regexParenthesesKana taKana
    replace regex = subRegex (mkRegex regex)
```

#### 括弧内文字列の変換

括弧内の文字列は、最初に`、`を区切り文字として分割した上で、不要な文字列と思われるものを取り除きます。この時に重要なのは町域名の漢字（CSVの9番目）の項目をベースにするということです。町域名漢字の括弧内文字列が区切られていても、半角カナのほうはそうなっていない場合があります。その場合は半角カナの値をそのまま使うようにします。

```haskell
import           Data.List.Split

regexUseInParentheses :: String
regexUseInParentheses = "[０-９]+区"

regexUseInParenthesesKana :: String
regexUseInParenthesesKana = "[0-9]+ｸ"

regexIgnoreInParentheses :: String
regexIgnoreInParentheses =
    "[０-９]|^その他$|^丁目$|^番地$|^地階・階層不明$|[０-９]*地割|成田国際空港内|次のビルを除く|^全域$"

convertInParentheses :: Postcode -> [(String, String)]
convertInParentheses p = filter shouldUse
    $ zip (splitInParentheses ta) (splitInParenthesesKana taKana)
  where
    shouldUse :: (String, String) -> Bool
    shouldUse (s, _) =
        s =~ regexUseInParentheses || not (s =~ regexIgnoreInParentheses)
    ta     = townArea p
    taKana = townAreaKana p

splitInParentheses :: String -> [String]
splitInParentheses str = case matchParentheses str of
    Just strInParenthes -> splitOn "、" strInParenthes
    Nothing             -> []

splitInParenthesesKana :: String -> [String]
splitInParenthesesKana str =
    maybe (repeat str) (splitOn "､") $ matchParenthesesKana str

matchParentheses :: String -> Maybe String
matchParentheses = matchParentheses' regexParentheses

matchParenthesesKana :: String -> Maybe String
matchParenthesesKana = matchParentheses' regexParenthesesKana

matchParentheses' :: String -> String -> Maybe String
matchParentheses' regex = fmap head . matchRegex (mkRegex regex)

```

#### その他

それ以外はCSV記載のまま出力（変換無し）です。

```haskell
convertTownArea :: Postcode -> [(String, String)]
convertTownArea p
...
    | otherwise
    = [(ta, taKana)]
...
```

## 出力結果

細かな調整は必要と思われるものの、目に付いた大きな点は対応できた気がしてきました。動かしてみます。

main関数はこんな感じで、結果は`ken_all.converted.csv`ファイルに出力されます。

```haskell
module Main where

import Lib

input = "KEN_ALL.CSV"
output = "ken_all.converted.csv"

main :: IO ()
main = convert input output
```

結果ですが、zipcloudさんのファイルとの差分は

zipcloud版行数：127,055
追加：126
削除：29
変更：77

という結果になりました。少し具体的に見てみましょう。

※記載したzipcloudさんの出力値は私の実装との比較のためにダブルクオーテーションを消してあります。zipcloudさんのデータにはKEN_ALL.CSVと同様の仕様でダブルクオーテーションが入っています。

KEN_ALL.CSVで

```
01214,"097  ","0970035","ﾎｯｶｲﾄﾞｳ","ﾜｯｶﾅｲｼ","ﾊﾞｯｶｲﾑﾗ(ﾊﾞｯｶｲ)","北海道","稚内市","抜海村（バッカイ）",1,0,0,0,0,0
```

はzipcloudバージョンでは

```
01214,097  ,0970035,ﾎｯｶｲﾄﾞｳ,ﾜｯｶﾅｲｼ,ﾊﾞｯｶｲﾑﾗ,北海道,稚内市,抜海村,1,0,0,0,0,0
```

ですが、私の実装では

```
01214,097  ,0970035,ﾎｯｶｲﾄﾞｳ,ﾜｯｶﾅｲｼ,ﾊﾞｯｶｲﾑﾗ,北海道,稚内市,抜海村,1,0,0,0,0,0
01214,097  ,0970035,ﾎｯｶｲﾄﾞｳ,ﾜｯｶﾅｲｼ,ﾊﾞｯｶｲﾑﾗﾊﾞｯｶｲ,北海道,稚内市,抜海村バッカイ,1,0,0,0,0,0
```

になってしまってました。これはつまり、`抜海村`と`バッカイ`は同一のものを指してるので重複である、と認識する必要があったようです。

他では

```
- KEN_ALL.CSV
01459,"07102","0710245","ﾎｯｶｲﾄﾞｳ","ｶﾐｶﾜｸﾞﾝﾋﾞｴｲﾁｮｳ","ﾌｸﾄﾐ(ﾌｸﾄﾐｴｲｼﾝ)","北海道","上川郡美瑛町","福富（福富瑛進）",1,0,0,0,0,0
- zipcloud
01459,07102,0710245,ﾎｯｶｲﾄﾞｳ,ｶﾐｶﾜｸﾞﾝﾋﾞｴｲﾁｮｳ,ﾌｸﾄﾐ,北海道,上川郡美瑛町,福富,1,0,0,0,0,0
- my実装
01459,07102,0710245,ﾎｯｶｲﾄﾞｳ,ｶﾐｶﾜｸﾞﾝﾋﾞｴｲﾁｮｳ,ﾌｸﾄﾐ,北海道,上川郡美瑛町,福富,1,0,0,0,0,0
01459,07102,0710245,ﾎｯｶｲﾄﾞｳ,ｶﾐｶﾜｸﾞﾝﾋﾞｴｲﾁｮｳ,ﾌｸﾄﾐﾌｸﾄﾐｴｲｼﾝ,北海道,上川郡美瑛町,福富福富瑛進,1,0,0,0,0,0
```

というのもありました。これも重複であることを知ってる必要がありそうです。

次はちょっと不明なもの

```
- KEN_ALL.CSV
03366,"02955","0295511","ｲﾜﾃｹﾝ","ﾜｶﾞｸﾞﾝﾆｼﾜｶﾞﾏﾁ","ｳｴﾉﾉ39ﾁﾜﾘ","岩手県","和賀郡西和賀町","上野々３９地割",0,0,0,0,0,0
- zipcloud
03366,02955,0295511,ｲﾜﾃｹﾝ,ﾜｶﾞｸﾞﾝﾆｼﾜｶﾞﾏﾁ,ｳｴﾉﾉ39ﾁﾜﾘ,岩手県,和賀郡西和賀町,上野々３９地割,0,0,0,0,0,0
- my実装
03366,02955,0295511,ｲﾜﾃｹﾝ,ﾜｶﾞｸﾞﾝﾆｼﾜｶﾞﾏﾁ,ｳｴﾉﾉ,岩手県,和賀郡西和賀町,上野々,0,0,0,0,0,0
```

数字+`地割`は削除している箇所が多い気がしますが、ここは残してあるようです。うーむ。

あと、これは結局対応ができたのですが、富山県に多いケースで

```
- KEN_ALL.CSV
16211,"93903","9390321","ﾄﾔﾏｹﾝ","ｲﾐｽﾞｼ","ｱｵｲﾀﾞﾆ","富山県","射水市","青井谷",0,0,0,1,0,0
16211,"93903","9390321","ﾄﾔﾏｹﾝ","ｲﾐｽﾞｼ","ｱｵｲﾀﾞﾆ(ｱﾗﾔｼｷ)","富山県","射水市","青井谷（新屋敷）",0,0,0,1,0,0
16211,"93903","9390321","ﾄﾔﾏｹﾝ","ｲﾐｽﾞｼ","ｱｵｲﾀﾞﾆ(ｺﾞｶﾝﾉ)","富山県","射水市","青井谷（五官野）",0,0,0,1,0,0
16211,"93903","9390321","ﾄﾔﾏｹﾝ","ｲﾐｽﾞｼ","ｱｵｲﾀﾞﾆ(ｻﾝﾉ)","富山県","射水市","青井谷（三野）",0,0,0,1,0,0
16211,"93903","9390321","ﾄﾔﾏｹﾝ","ｲﾐｽﾞｼ","ｱｵｲﾀﾞﾆ(ﾆｼﾀﾆ)","富山県","射水市","青井谷（西谷）",0,0,0,1,0,0
16211,"93903","9390321","ﾄﾔﾏｹﾝ","ｲﾐｽﾞｼ","ｱｵｲﾀﾞﾆ(ﾐｽﾞｶﾐﾀﾆ)","富山県","射水市","青井谷（水上谷）",0,0,0,1,0,0
```

というのを、最初の頃

```
- my実装
16211,93903,9390321,ﾄﾔﾏｹﾝ,ｲﾐｽﾞｼ,ｱｵｲﾀﾞﾆ,富山県,射水市,青井谷,0,0,0,1,0,0
16211,93903,9390321,ﾄﾔﾏｹﾝ,ｲﾐｽﾞｼ,ｱｵｲﾀﾞﾆ,富山県,射水市,青井谷,0,0,0,1,0,0
16211,93903,9390321,ﾄﾔﾏｹﾝ,ｲﾐｽﾞｼ,ｱｵｲﾀﾞﾆｱﾗﾔｼｷ,富山県,射水市,青井谷新屋敷,0,0,0,1,0,0
16211,93903,9390321,ﾄﾔﾏｹﾝ,ｲﾐｽﾞｼ,ｱｵｲﾀﾞﾆ,富山県,射水市,青井谷,0,0,0,1,0,0
16211,93903,9390321,ﾄﾔﾏｹﾝ,ｲﾐｽﾞｼ,ｱｵｲﾀﾞﾆｺﾞｶﾝﾉ,富山県,射水市,青井谷五官野,0,0,0,1,0,0
16211,93903,9390321,ﾄﾔﾏｹﾝ,ｲﾐｽﾞｼ,ｱｵｲﾀﾞﾆ,富山県,射水市,青井谷,0,0,0,1,0,0
16211,93903,9390321,ﾄﾔﾏｹﾝ,ｲﾐｽﾞｼ,ｱｵｲﾀﾞﾆｻﾝﾉ,富山県,射水市,青井谷三野,0,0,0,1,0,0
16211,93903,9390321,ﾄﾔﾏｹﾝ,ｲﾐｽﾞｼ,ｱｵｲﾀﾞﾆ,富山県,射水市,青井谷,0,0,0,1,0,0
16211,93903,9390321,ﾄﾔﾏｹﾝ,ｲﾐｽﾞｼ,ｱｵｲﾀﾞﾆﾆｼﾀﾆ,富山県,射水市,青井谷西谷,0,0,0,1,0,0
16211,93903,9390321,ﾄﾔﾏｹﾝ,ｲﾐｽﾞｼ,ｱｵｲﾀﾞﾆ,富山県,射水市,青井谷,0,0,0,1,0,0
```

となってしまって、町域名が`青井谷`の完全に重複したレコードが複数できてしまってました。今回これは処理時に郵便番号が変わった時点の変換後行を保持して、以降の行が同一であれば出力しない、というロジックでフィルタすることができました。ただ、`KEN_ALL.CSV`は同一の郵便番号が全然離れた行に現れたりもするので、完全対応はDBなどに入れて処理するしかない感じがします。

また、今回の処理の副作用で下記のような削除行が出てしまいました。これは私の実装のほうが重複無くできているように見えますが、よく見ると末尾の方の0/1フラグが微妙に違ってます。zipcloudさんのはこの辺もちゃんと見て重複除去を行っているようです。

```
- KEN_ALL.CSV
16211,"93903","9390341","ﾄﾔﾏｹﾝ","ｲﾐｽﾞｼ","ｻﾝｶﾞ","富山県","射水市","三ケ",0,1,0,1,0,0
16211,"93903","9390341","ﾄﾔﾏｹﾝ","ｲﾐｽﾞｼ","ｻﾝｶﾞ(ｱﾀｺﾞ)","富山県","射水市","三ケ（愛宕）",0,0,0,1,0,0
- zipcloud
16211,93903,9390341,ﾄﾔﾏｹﾝ,ｲﾐｽﾞｼ,ｻﾝｶﾞ,富山県,射水市,三ケ,0,1,0,1,0,0
16211,93903,9390341,ﾄﾔﾏｹﾝ,ｲﾐｽﾞｼ,ｻﾝｶﾞ,富山県,射水市,三ケ,0,0,0,1,0,0
- my実装
16211,93903,9390341,ﾄﾔﾏｹﾝ,ｲﾐｽﾞｼ,ｻﾝｶﾞ,富山県,射水市,三ケ,0,1,0,1,0,0
```

ただ、よくわからないのは他の県ではこの手のは括弧内に`青井谷（新屋敷、五官野、三野、西谷）`と書いてることも多いのですけど、富山県などでは何でこうなってるんでしょうか。いや、そんな疑問を持ってはいけませんね。私が悪いです。

他にも細かな相違点はありましたが、多くは重複行が残ってる部分でしたので、それが取り除かれればかなり目標近くに行けそうです。

## 結論

正直このやっつけ仕事で127,055行の見本ファイルとの差分が240件ぐらい、そのうち追加行が126程度ということで、結構満足してしまってるのですが、業務で実際に使うとなると厳しいでしょうか。メンテのことなども考慮すると、職人さんの作ったものを利用するか、商用のものが安心ですね。

と、ここまでやってきてふと思うのは、何で今の時代にこういうファイルが~~放置~~提供され、且つ更新し続けているのか、という点です。町域名が複数行にまたがっているということを知った時点で、このファイルそもそも大丈夫なのかな？という疑問を普通は持ってしまうでしょう。

おそらくは住所と郵便番号、管理されてきたシステムの歴史にまつわるものかと思われますが、どこかでキレイになって欲しいですね。

それはともかく、Haskell環境は本当に昔に比べてよくなりました。型シグネチャ見るために都度hackageのドキュメント見たり、慣れないEmacsの設定したりといったことは今回不要で、快適に実装ができました。ずっとHaskellからは離れてしまってたのですが、今後は積極的に使っていこうと思います。

## 参考
- [郵便番号データ配信サービス zipcloud](http://zipcloud.ibsnet.co.jp/)
- [vscode と haskell-ide-engine で Haskell 開発環境を構築する](https://qiita.com/waddlaw/items/b83cd10311200095fe87)
- [郵便番号から住所を検索するサービスにまともなものがない - ぐるぐる～](http://bleis-tift.hatenablog.com/entry/20080531/1212217681)
- [FizzBuzz 問題どや顔で解くひとなんかよりも "KEN_ALL.csv" をうまく扱える人の方が社会的貢献度高い](https://togetter.com/li/352382)