Backbone.jsを使用した郵便番号検索デモ


以前に作成した以下の二つを組み合わせて、実際に動作するデモを作ってみました。

* [Backbone.jsをTypeScriptから使用する](http://qiita.com/MizunagiKB/items/eb949ad6f8ebd1c268d5)
* [郵便局配布の郵便番号データをJSONに加工](http://qiita.com/
MizunagiKB/items/4404556727366c0e8d30)

こんな感じで動作しています。

|[郵便番号から住所の検索](http://mizuvm01.cloudapp.net/gadgets/couch/address.html)|[住所絞り込みによる郵便番号の検索](http://mizuvm01.cloudapp.net/gadgets/couch/zipcode.html)|
|---|---|
|![ss_couch_address.jpg](https://qiita-image-store.s3.amazonaws.com/0/28538/d4ff549d-823a-98ba-d8a1-f82256a6a546.jpeg "ss_couch_address.jpg")|![ss_couch_zipcode.jpg](https://qiita-image-store.s3.amazonaws.com/0/28538/23d5ef9d-747e-6c36-26f1-873e20f373fb.jpeg "ss_couch_zipcode.jpg")|

郵便番号情報からJSONを生成して、結果をCouchDBに格納、さらにwebブラウザからBackbone.js経由で利用しています。


## 郵便番号検索デモの説明
### 郵便番号から住所の検索
http://mizuvm01.cloudapp.net/gadgets/couch/address.html

ハイフン無しで郵便番号を入力すると、存在していればその郵便番号に一致する住所が表示されます。（入力は半角数字のみとなります。）

7桁未満の値を入力した場合は、前方一致による検索を行い、複数の住所候補が存在する場合は検索結果から16件のみが表示されます。

> これを書いている時点で日本の郵便番号は7桁までしかありませんので、8桁以上入力した場合は、検索結果は「なし」となります。

### 住所絞り込みによる郵便番号の検索

都道府県、市区町村の順に絞り込みを行い、知りたい郵便番号候補が表示されます。

Prefecture（都道府県）のドロップダウンから絞り込みたい都道府県を選択すると、選択した都道府県内のMunicipality（市区町村）がドロップダウンで選択出来るようになります。
PrefectureとMunicipalityが決定されると、最終的な候補がすべて表示されます。

> こちらのデモはソート処理を実装していないため、実行タイミング毎に並びが異なる可能性があります。

## 実装方法について
### 住所データのCouchDBへの登録

Pythonを使用して **郵便局配布の郵便番号データをJSONに加工** の出力結果をCouchDBに書き込んでいます。

正常にCouchDBに書き込めていれば、70MBytes程度のデータベースが出来ます。作業手順によっては、もっと大きなサイズになっている場合があります。（その場合は最適化する事で最適化が可能です）

```python:update_zipdata.py
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------ import(s)
import sys
import json
import couchdb

# ------------------------------------------------------------------- param(s)
USERNAME = "USERNAME"
PASSWORD = "PASSWORD"
COUCHDB_ADDR = "localhost"
COUCHDB_PORT = 5984

# ------------------------------------------------------------------- class(s)
# -----------------------------------------------------------------function(s)
# ============================================================================
##
#
def main():

    with open(sys.argv[1], "rb") as hFile:
        dict_zip = json.load(hFile)

        oCCouch = couchdb.Server("http://%s:%s@%s:%d" % (USERNAME, PASSWORD, COUCHDB_ADDR, COUCHDB_PORT))
        oCDB = oCCouch["zipcode"]

        nProgress = 0

        for k, dict_o in dict_zip.items():
            nProgress += 1
            print "%8d/%8d" % (nProgress, len(dict_zip)), k
            try:
                dictRecord = oCDB["JP:%s" % (k,)]
            except couchdb.http.ResourceNotFound:
                dictRecord = {
                    "_id": "JP:%s" % (k,),
                    "country": "JP"
                }
            dictRecord["address_list"] = dict_o["address_list"]

            oCDB.save(dictRecord)


if __name__ == "__main__":
    main()
```

上記スクリプトでは、データが重複しないように_idに "JP:" というのを先頭に追加しています。
データベースでいう一意制約（UNIQUE）を使用するには、CouchDBの場合_idを使用することでデータベース内で一意性を保つことが可能です。

ここでは郵便番号を登録する際に、ISO 3166-1 alpha-2に準拠した国コードを先頭に追加する、というのを想定した実装をしています。


### CouchDB側にviewを追加

CouchDBには以下の様な形式で格納されています。

```javascript:Document
{
   "_id": "JP:0010000",
   "_rev": "2-5e4eab07eca6ffd94643d40136107da1",
   "country": "JP",
   "address_list": [
       {
           "ja_kana": [
               "ホツカイドウ",
               "サツポロシキタク"
           ],
           "ja": [
               "北海道",
               "札幌市北区"
           ],
           "ja_rome": [
               "HOKKAIDO",
               "SAPPORO SHI KITA KU"
           ]
       }
   ]
}
```

CouchDBはドキュメントの取得をHTTP/GETで行えるため、

```bash
http://<CouchDB Host>:5984/zipcode/JP%3A0010000
```

直接ドキュメントを開くことが出来ます。

また、オプションを指定することもできますので、

```bash
$ curl "http://<CouchDB Host>:5984/zipcode/_all_docs?startkey=%22JP:100000%22&endkey=%22JP:100000Z%22&limit=10"
{"total_rows":119762,"offset":14102,"rows":[
{"id":"JP:1000000","key":"JP:1000000","value":{"rev":"2-c6e154edf794991229bb1590159ef11e"}},
{"id":"JP:1000001","key":"JP:1000001","value":{"rev":"2-faaef5d14fb28baefd4dc654eb2d827b"}},
{"id":"JP:1000002","key":"JP:1000002","value":{"rev":"2-ca2ee4de55fcf720b1b57c4d922f97ce"}},
{"id":"JP:1000003","key":"JP:1000003","value":{"rev":"3-66ae4b75c2b36683b4b77e5b33d9200a"}},
{"id":"JP:1000004","key":"JP:1000004","value":{"rev":"4-8c9fbce791a1cfa85113b40e2fa4567b"}},
{"id":"JP:1000005","key":"JP:1000005","value":{"rev":"2-f427efb1a039eba3612f1a9a37f920e1"}},
{"id":"JP:1000006","key":"JP:1000006","value":{"rev":"3-6b98c5dbe9f318d215a84d53e1d5d751"}}
]}
```

上記の様な範囲検索も可能です。

既にこれで良い気もしますが、CouchDBにはviewと呼ばれる機能が付いていますのでそれを利用します。


#### 郵便番号検索用のviewを作成

郵便番号から住所を検索する為のviewを作成しますが、対象が日本専用ですので検索範囲をあらかじめ日本だけに絞り込んでおきます。

```javascript:search_zipcode
// Map Function
function(doc)
{
  if(doc.country == "JP")
  {
    emit(doc._id, null);
  }
}
```

#### 住所絞り込み用のviewを作成

住所絞り込みを使用するには、Key指定部に配列を与える事で配列数分の絞り込みを行う事が出来ます。
（group_levelとReduceを有効にする必要があります。）

```javascript:search_address
// Map Function
function(doc)
{
  for(var n = 0; n < doc.address_list.length; n++)
  {
    if(doc.country == "JP")
    {
      if(doc.address_list[n].ja)
      {
        emit(doc.address_list[n].ja, null);
      }
    }
  }
}

// Reduce Function
_count
```

このviewを普通に呼び出すと、

```bash
$curl "http://<CouchDB Host>:5984/zipcode/_design/jp_zipcode/_view/search_address"
{"rows":[
{"key":null,"value":123590}
]}
```

と、単にドキュメント数がカウントされるだけですが、group_levelを指定する事で

```bash
$ curl "http://<CouchDB Host>:5984/zipcode/_design/jp_zipcode/_view/search_address?group_level=1"
{"rows":[
{"key":["三重県"],"value":2475},
{"key":["京都府"],"value":6441},
{"key":["佐賀県"],"value":872},
 .
 .
 .
{"key":["鹿児島県"],"value":1452}
]}
```

集計処理ををグルーピングする事が出来ます。

これにstartkeym endkeyを組み合わせる事で絞り込みを実現しています。


### 郵便番号検索を行うTypeScriptコード

```javascript:mvc_zipcode.ts
// ===========================================================================
/*!
 * @brief Zipcode
 * @author @MizunagiKB
 */
// -------------------------------------------------------------- reference(s)
/// <reference path="./DefinitelyTyped/jquery/jquery.d.ts"/>
/// <reference path="./DefinitelyTyped/bootstrap/bootstrap.d.ts"/>
/// <reference path="./DefinitelyTyped/backbone/backbone.d.ts"/>
/// <reference path="./DefinitelyTyped/hogan/hogan.d.ts"/>


// ---------------------------------------------------------------- declare(s)

namespace zipcode {

    // ---------------------------------------------------------- interface(s)
    // -------------------------------------------------------------- class(s)
    // -----------------------------------------------------------------------
    /*!
     */
    export class model_CZipcode extends Backbone.Model {

        //
        constructor(attributes?: any, options?: any) {
            super(attributes, options);
        }
    }

    // -----------------------------------------------------------------------
    /*!
     */
    export class model_CAddress1 extends Backbone.Model {

        //
        constructor(attributes?: any, options?: any) {
            super(attributes, options);
        }
    }

    // -----------------------------------------------------------------------
    /*!
     */
    export class model_CAddress2 extends Backbone.Model {

        //
        constructor(attributes?: any, options?: any) {
            super(attributes, options);
        }
    }

    // -----------------------------------------------------------------------
    /*!
     */
    export class view_CZipcode extends Backbone.View<model_CZipcode> {

        //
        constructor(options?: Backbone.ViewOptions<model_CZipcode>, dictTemplate?: any) {
            super(options);

            this.listenTo(this.model, "change", this.render);
        }

        //
        events(): Backbone.EventsHash {
            return {
                "click button#id_btn_search": this.evt_search,
            }
        }

        evt_search(oCEvt: any) {
            const strZipcode: string = $("#id_input_zipcode").val();

            this.model.url = "/db/zipcode/_design/jp_zipcode/_view/search_zipcode";
            this.model.fetch(
                {
                    data: {
                        startkey: JSON.stringify("JP:" + strZipcode),
                        endkey: JSON.stringify("JP:" + strZipcode + "Z"),
                        limit: 16,
                        include_docs: true
                    }
                }
            );
        }

        //
        render() {
            let listAddress: Array<string> = [];

            for (let r: number = 0; r < this.model.attributes.rows.length; r++) {
                const doc = this.model.attributes.rows[r].doc;

                for (let n: number = 0; n < doc.address_list.length; n++) {
                    listAddress.push(
                        "<tr><td>" + doc._id + "</td><td>" + doc.address_list[n].ja.join(" ") + "<br /><small>" + doc.address_list[n].ja_rome.join(" ") + "</small></td></tr>"
                    );
                }
            }

            $("#id_zipcode_result").html(listAddress.join(""));

            return this;
        }
    }
}



// --------------------------------------------------------------------- [EOF]

```

### 住所絞り込みを行うTypeScriptコード

```javascript:mvc_address.ts
// ===========================================================================
/*!
 * @brief Address
 * @author @MizunagiKB
 */
// -------------------------------------------------------------- reference(s)
/// <reference path="./DefinitelyTyped/jquery/jquery.d.ts"/>
/// <reference path="./DefinitelyTyped/bootstrap/bootstrap.d.ts"/>
/// <reference path="./DefinitelyTyped/backbone/backbone.d.ts"/>
/// <reference path="./DefinitelyTyped/hogan/hogan.d.ts"/>


// ---------------------------------------------------------------- declare(s)

namespace zipcode {

    // ---------------------------------------------------------- interface(s)
    // -------------------------------------------------------------- class(s)
    // -----------------------------------------------------------------------
    /*!
     */
    export class model_CPrefecture extends Backbone.Model {

        //
        constructor(attributes?: any, options?: any) {
            super(attributes, options);
        }
    }

    // -----------------------------------------------------------------------
    /*!
     */
    export class model_CMunicipality extends Backbone.Model {
        current_prefecture: string = "";
        //
        constructor(attributes?: any, options?: any) {
            super(attributes, options);
        }
    }

    // -----------------------------------------------------------------------
    /*!
     */
    export class model_CBlock extends Backbone.Model {
        current_prefecture: string = "";
        current_municipality: string = "";

        //
        constructor(attributes?: any, options?: any) {
            super(attributes, options);
        }
    }

    // -----------------------------------------------------------------------
    /*!
     */
    export class view_CPrefecture extends Backbone.View<model_CPrefecture> {
        sub_model: model_CMunicipality = null;

        //
        constructor(options?: Backbone.ViewOptions<model_CPrefecture>, sub_model?: model_CMunicipality) {
            super(options);

            this.sub_model = sub_model;
            this.listenTo(this.model, "change", this.render);
        }

        //
        events(): Backbone.EventsHash {
            return {
                "click .dropdown-prefecture a": this.evt_view_change,
            }
        }

        evt_view_change(oCEvt: any) {
            const strPrefecture: string = $(oCEvt.currentTarget).data("name");

            $("#id_current_prefecture").val(strPrefecture);

            this.sub_model.current_prefecture = strPrefecture;
            this.sub_model.url = "/db/zipcode/_design/jp_zipcode/_view/search_address";
            this.sub_model.fetch(
                {
                    data: {
                        group_level: 2,
                        startkey: JSON.stringify([strPrefecture, ""]),
                        endkey: JSON.stringify([strPrefecture + "Z", ""])
                    }
                }
            );
        }

        //
        render() {
            let listPrefecture: Array<string> = [];

            for (let n: number = 0; n < this.model.attributes.rows.length; n++) {
                const dictItem = this.model.attributes.rows[n];

                listPrefecture.push("<li><a data-name=\"" + dictItem.key[0] + "\" href=\"javascript:void(0);\">" + dictItem.key[0] + "</a></li>");
            }

            $("#id_current_prefecture").val("");
            $("#id_dropdown_prefecture").html(listPrefecture.join(""));

            return this;
        }
    }

    // -----------------------------------------------------------------------
    /*!
     */
    export class view_CMunicipality extends Backbone.View<model_CMunicipality> {
        sub_model: model_CBlock = null;

        //
        constructor(options?: Backbone.ViewOptions<model_CMunicipality>, sub_model?: model_CBlock) {
            super(options);

            this.sub_model = sub_model;
            this.listenTo(this.model, "change", this.render);
        }

        //
        events(): Backbone.EventsHash {
            return {
                "click .dropdown-municipality a": this.evt_view_change,
            }
        }

        evt_view_change(oCEvt: any) {
            const strPrefecture: string = this.model.current_prefecture;
            const strMunicipality: string = $(oCEvt.currentTarget).data("name");

            $("#id_current_municipality").val(strMunicipality);

            this.sub_model.current_prefecture = strPrefecture;
            this.sub_model.current_municipality = strMunicipality;
            this.sub_model.url = "/db/zipcode/_design/jp_zipcode/_view/search_address";
            this.sub_model.fetch(
                {
                    data: {
                        startkey: JSON.stringify([strPrefecture, strMunicipality]),
                        endkey: JSON.stringify([strPrefecture, strMunicipality + "Z"]),
                        include_docs: true,
                        reduce: false
                    }
                }
            );
        }

        //
        render() {
            let listResult: Array<string> = [];

            for (let n: number = 0; n < this.model.attributes.rows.length; n++) {
                const dictItem = this.model.attributes.rows[n];

                listResult.push(
                    "<li><a data-name=\"" + dictItem.key[1] + "\" href=\"javascript:void(0);\">" + dictItem.key[1] + "</a></li>"
                );
            }

            $("#id_current_municipality").val("");
            $("#id_dropdown_municipality").html(listResult.join(""));

            $("#id_address_result").html("");

            return this;
        }
    }

    // -----------------------------------------------------------------------
    /*!
     */
    export class view_CBlock extends Backbone.View<model_CBlock> {

        //
        constructor(options?: Backbone.ViewOptions<model_CBlock>, dictTemplate?: any) {
            super(options);

            this.listenTo(this.model, "change", this.render);
        }

        //
        events(): Backbone.EventsHash {
            return {
            }
        }

        //
        render() {
            let listResult: Array<string> = [];

            for (let r: number = 0; r < this.model.attributes.rows.length; r++) {
                const doc = this.model.attributes.rows[r].doc;

                for (let n: number = 0; n < doc.address_list.length; n++) {
                    if (doc.address_list[n].ja.length == 2) {
                        listResult.push(
                            "<tr><td>*<br /><small>*</small></td><td>" + doc._id + "</td></tr>"
                        );
                    } else if (doc.address_list[n].ja.length > 2) {
                        listResult.push(
                            "<tr><td>" + doc.address_list[n].ja[2] + "<br /><small>" + doc.address_list[n].ja_rome[2] + "</small></td><td>" + doc._id + "</td></tr>"
                        );
                    }
                }
            }

            $("#id_address_result").html(listResult.join(""));

            return this;
        }
    }
}



// --------------------------------------------------------------------- [EOF]
```

### 初期化コード

```javascript:zipcode.ts
// ===========================================================================
/*!
 * @brief main
 * @author @MizunagiKB
 */
// -------------------------------------------------------------- reference(s)
/// <reference path="./DefinitelyTyped/jquery/jquery.d.ts"/>
/// <reference path="./DefinitelyTyped/bootstrap/bootstrap.d.ts"/>
/// <reference path="./DefinitelyTyped/backbone/backbone.d.ts"/>
/// <reference path="./DefinitelyTyped/hogan/hogan.d.ts"/>
/// <reference path="./mvc_address.ts"/>
/// <reference path="./mvc_zipcode.ts"/>


// ---------------------------------------------------------------- declare(s)

namespace zipcode {

    // ---------------------------------------------------------- interface(s)
    // -------------------------------------------------------------- class(s)
    // ----------------------------------------------------------- function(s)
    // =======================================================================
    export function main() {
        let oCModelCZipcode: model_CZipcode = null;
        let oCModelCPrefecture: model_CPrefecture = null;
        let oCModelCMunicipality: model_CMunicipality = null;
        let oCModelCBlock: model_CBlock = null;
        let oCViewCZipcode: view_CZipcode = null;
        let oCViewCPrefecture: view_CPrefecture = null;
        let oCViewCMunicipality: view_CMunicipality = null;
        let oCViewCBlock: view_CBlock = null;

        oCModelCZipcode = new model_CZipcode();

        oCViewCZipcode = new view_CZipcode(
            {
                el: "body",
                model: oCModelCZipcode
            }
        );

        oCModelCPrefecture = new model_CPrefecture();
        oCModelCMunicipality = new model_CMunicipality();
        oCModelCBlock = new model_CBlock();

        oCViewCPrefecture = new view_CPrefecture(
            {
                el: "body",
                model: oCModelCPrefecture
            },
            oCModelCMunicipality
        );

        oCViewCMunicipality = new view_CMunicipality(
            {
                el: "body",
                model: oCModelCMunicipality
            },
            oCModelCBlock
        );

        oCViewCBlock = new view_CBlock(
            {
                el: "body",
                model: oCModelCBlock
            }
        );

        oCModelCPrefecture.url = "/db/zipcode/_design/jp_zipcode/_view/search_address";
        oCModelCPrefecture.fetch(
            {
                data: {
                    group_level: 1
                }
            }
        );
    }
}



// --------------------------------------------------------------------- [EOF]
```