y-matsui::weblog

電子楽器、音楽、コンピュータ、プログラミング、雑感。面倒くさいオヤジの独り言

住所から緯度経度を応答するWebサービス(ジオコーディング)できた

前エントリ”自前でgeocodingの手順”に従って、実際に作ってみた。→address2point
実装にあたって、いくつかの制限を設けた。
 ・都道府県→市群区町村→丁目の順で記述されている前提とし、前方一致とする
  (不足しているレベルを自動で補完しない)
 ・番、番地までのレベルとする
  (それ以降は記述があっても切り落とす)
 ・検索結果が指定数を超えた場合は、エラーを返す
 ・指定されたクエリで検索して、結果が該当しなければ丁目単位で検索し、結果を最大10件返す
 ・実行時間にタイムアウトを設定(60秒)

さらに、検索結果の確かさを判定するため、トータル件数の逆数を返すようにした。
番地まで完全に一致した場合で、複数レコードが無い場合に1/1となる。
番地レコードが複数存在する場合や丁目までの検索で前方一致した場合は、1/n

■実装手順
1:街区レベル位置参照情報の住所を結合
都道府県(c_prefname)、市郡区町村(c_cityname)、丁目(c_townname)、番地(c_streetname)を結合し、c_address列として定義。

update tbl_address set c_address = c_prefname||c_cityname||c_townname||c_streetname

※フィールド文字列を結合するUpdate式で数時間掛かった。

2:住所の読みかえ
・全角英数を半角化
・空白除去
・n丁目をハイフンに置き換え
・番、番地の置き換え

当初SQLのReplaceとTranslateで置き換えようと考えていたが、1800万件*置き換え処理数のSQLが実行されるという恐ろしい内容なので、やり方を改めた。
Update処理専用のコンソールプログラムを書いて、レコードを一つづつ読み出し、アップデートすることに。
これなら最大でも1800万件*2回(selectとupdate)の処理で済む。→夜通し掛かって処理完了。(18107015件を対象に、2907002件の住所文字列を置換処理した)

※実際、SQLを流す方法の場合、あまりにも処理が重すぎて返ってこないため、諦めた。

3:クエリの書き換え
2のルールに従って、クエリとして与えられる住所文字列も置き換え。

//住所文字列の置き換え
private string replaceAddress(string address)
{
string ret = "";
if *1
{
return "";
}else
{
ret=address;
}

//空白の除去
ret=ret.Replace(' ',' ');
ret=ret.Trim();

//置き換え文字列を配列に入れてひとつずつ変換
//一文字単位ではない置き換えも含むので、正規表現を使わない
//?〜? →1-10
//1〜0→1-0
//a〜z→a-z
//A〜Z→A-Z
//一丁目〜五十丁目→1-〜50- (最大は北海道の43丁目)

//一丁目〜五十丁目→1-〜50- (最大は北海道の43丁目)

string instring = new string {
"?", "?", "?", "?", "?", "?", "?", "?", "?", "?",
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
"十一丁目", "十二丁目", "十三丁目", "十四丁目", "十五丁目", "十六丁目", "十七丁目", "十八丁目", "十九丁目", "二十丁目",
"二十一丁目", "二十二丁目", "二十三丁目", "二十四丁目", "二十五丁目", "二十六丁目", "二十七丁目", "二十八丁目", "二十九丁目", "三十丁目",
"三十一丁目", "三十二丁目", "三十三丁目", "三十四丁目", "三十五丁目", "三十六丁目", "三十七丁目", "三十八丁目", "三十九丁目", "四十丁目",
"四十一丁目", "四十二丁目", "四十三丁目", "四十四丁目", "四十五丁目", "四十六丁目", "四十七丁目", "四十八丁目", "四十九丁目", "五十丁目",
"一丁目", "二丁目", "三丁目", "四丁目", "五丁目", "六丁目", "七丁目", "八丁目", "九丁目", "十丁目",
"11丁目", "12丁目", "13丁目", "14丁目", "15丁目", "16丁目", "17丁目", "18丁目", "19丁目", "20丁目",
"21丁目", "22丁目", "23丁目", "24丁目", "25丁目", "26丁目", "27丁目", "28丁目", "29丁目", "30丁目",
"31丁目", "32丁目", "33丁目", "34丁目", "35丁目", "36丁目", "37丁目", "38丁目", "39丁目", "40丁目",
"41丁目", "42丁目", "43丁目", "44丁目", "45丁目", "46丁目", "47丁目", "48丁目", "49丁目", "50丁目",
"1丁目", "2丁目", "3丁目", "4丁目", "5丁目", "6丁目", "7丁目", "8丁目", "9丁目", "10丁目",
"11丁目", "12丁目", "13丁目", "14丁目", "15丁目", "16丁目", "17丁目", "18丁目", "19丁目", "20丁目",
"21丁目", "22丁目", "23丁目", "24丁目", "25丁目", "26丁目", "27丁目", "28丁目", "29丁目", "30丁目",
"31丁目", "32丁目", "33丁目", "34丁目", "35丁目", "36丁目", "37丁目", "38丁目", "39丁目", "40丁目",
"41丁目", "42丁目", "43丁目", "44丁目", "45丁目", "46丁目", "47丁目", "48丁目", "49丁目", "50丁目",
"1丁目", "2丁目", "3丁目", "4丁目", "5丁目", "6丁目", "7丁目", "8丁目", "9丁目", "10丁目",
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
};
string outstring=new string{
"1","2","3","4","5","6","7","8","9","0",
"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z",
"a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z",
"11-","12-","13-","14-","15-","16-","17-","18-","19-","20-",
"21-","22-","23-","24-","25-","26-","27-","28-","29-","30-",
"31-","32-","33-","34-","35-","36-","37-","38-","39-","40-",
"41-","42-","43-","44-","45-","46-","47-","48-","49-","50-",
"1-","2-","3-","4-","5-","6-","7-","8-","9-","10-",
"11-","12-","13-","14-","15-","16-","17-","18-","19-","20-",
"21-","22-","23-","24-","25-","26-","27-","28-","29-","30-",
"31-","32-","33-","34-","35-","36-","37-","38-","39-","40-",
"41-","42-","43-","44-","45-","46-","47-","48-","49-","50-",
"1-","2-","3-","4-","5-","6-","7-","8-","9-","10-",
"11-","12-","13-","14-","15-","16-","17-","18-","19-","20-",
"21-","22-","23-","24-","25-","26-","27-","28-","29-","30-",
"31-","32-","33-","34-","35-","36-","37-","38-","39-","40-",
"41-","42-","43-","44-","45-","46-","47-","48-","49-","50-",
"1-","2-","3-","4-","5-","6-","7-","8-","9-","10-",
"1","2","3","4","5","6","7","8","9","0",
}; for (int i = 0; i < instring.Length; i++)
{
if *2
{
ret = ret.Replace(instring[i], outstring[i]);
}
}

//番地、番以降の住所と番地・番の除去
int loc=ret.IndexOf("-"); //丁目(ハイフン)の位置を基点にして”番”を探す
if(ret.IndexOf("番",loc+1)>loc+1)
{
loc = ret.IndexOf("番", loc + 1);
ret = ret.Substring(0,loc);
//番地、番の除去
//ret = ret.Replace("番地", "");
//ret = ret.Replace("番", "");
}


//末端がハイフンで終わる場合はハイフンを除去
if(ret.EndsWith("-"))
{
ret=ret.Substring(0,ret.Length-1);
}

////正規表現で変換する(準備中)
////ret=Regex.Replace(ret,,);


return ret;
}

・アドレスのマッチング処理
public string address2point(string address)
{
string qaddress="";

if *3 == false))
{
return "住所の指定が不正";
}else
{
qaddress=address;
}

//住所文字列のクリーニング(置き換え)
qaddress=replaceAddress(address);

string ConnectionString = "Server=[サーバIPアドレス];" +
"Port=5432;" +
"Database=address;" +
"Encoding=UNICODE;" +
"User Id=[DBユーザID];" +
"Password=[パスワード];" +
"Pooling=true;" +
"CommandTimeout=60;"+
"ConnectionLifeTime=30;"+
"MaxPoolSize=50;" +
"MinPoolSize=10;";
string s = "";
string q = ""; //query
string w = ""; //where
string o = ""; //order
string t = ""; //top
int limit = 1000; //返却する最大件数

//接続を開く
NpgsqlConnection conn = new NpgsqlConnection(ConnectionString);
try
{
conn.Open();
}
catch (Exception ex)
{
s = "" + ex.Message + "";
return s;
}

//検索条件(前方一致)
w = " (c_address like '" + qaddress +"%')";

//レコードを取得する前に件数を確認する
q = "SELECT COUNT(n_id) from tbl_address where" + w;
double reccnt = 0;
reccnt = checkAddress(conn, q);

if (reccnt == 0)
{
//番地までで再検索する(最初のハイフン+任意桁の半角数字までの住所)
int loc = qaddress.IndexOf('-');
Regex reg=new Regex("[0-9]");
if ( loc> 0)
{
int j=qaddress.Length-loc;
qaddress += " ";
string tmpaddress = qaddress;
for(int i=1;i 0)
{
w = " (c_address like '" + qaddress + "%')";
q = "SELECT COUNT(n_id) from tbl_address where" + w;
reccnt = checkAddress(conn, q);
if (reccnt == 0)
{
s="\r\n";
s+="\t\r\n";
s += "\t\t

" + address + "
\r\n";
s += "\t\t" + qaddress + "\r\n";
s += "\t\t0";
s += "\t\t\n";
s += "\t\t\n";
s += "\t\t\n";
s += "\t\t\n";
s += "\t\t\n";
s += "\t\t-1\r\n";
s += "\t\t-1\r\n";
s+="\t\t-1,-1\r\n";
s+= "\t\r\n";
s+="\r\n";
conn.Close();
return s;
}
}
}


//件数が多ければデータを取得しない
if (reccnt > limit)
{
s = "結果件数:" + reccnt + "が上限" + limit + "件を超えました。";
return s;
}


//レコードを取得するSQL
t = " offset 0 limit 1";
o = " ORDER BY c_address";
q = "SELECT * from tbl_address where" + w + o + t;

NpgsqlDataAdapter da;
DataTable dt;
da = new NpgsqlDataAdapter(q, conn);
dt = new DataTable();
da.Fill(dt);

s += "\n";

if (dt.Rows.Count > 0)
{
foreach (DataRow row in dt.Rows)
{
s += "\t\r\n";
s += "\t\t

" + address + "
\r\n";
s += "\t\t" + qaddress + "\r\n";
s += "\t\t1/" + reccnt.ToString() +"";
s += "\t\t" + row["n_id"].ToString() + "\n";
s += "\t\t" + row["c_prefname"].ToString() + "\n";
s += "\t\t" + row["c_cityname"].ToString() + "\n";
s += "\t\t" + row["c_townname"].ToString() + "\n";
s += "\t\t" + row["c_streetname"].ToString() + "\n";
s += "\t\t" + row["n_lat"].ToString() + "\n";
s += "\t\t" + row["n_lon"].ToString() + "\n";
s += "\t\t" + row["n_lat"].ToString() + "," + row["n_lon"].ToString() + "\n";
s += "\t\n";
//i += 1;
//if (i > limit)
//{
// s += "件数が" + i + "を超えました。";
// break; //foreachループを抜ける
//}
}
}
s += "\n";

//閉じる
conn.Close();

//コネクションを切断する
conn.Dispose();
conn = null;

return s;
}



■結果
1秒以内に応答を得られる。
(前方一致の効果か)

※これを使って数万件単位のジオコーディングをしたら、どれくらいの時間と精度が得られるのだろうか?

→後日談:一件辺り20ms程度の処理速度が得られた。

■これもできない、あれもできない
googleさん、geocoding.jpさん、位置参照技術を用いたツールとユーティリティ@東大さん、住所正規化コンバータさんなどと比較して、”非常に貧弱”な今回のなんちゃってgeocoding Webサービス
あれもできない、これもできないの目白押しだ。
以下、ニーズがそこそこありそうな検索要求をリスト化。
・クエリの補完
 →住所に都道府県名が無かったり、”市”、”郡”、”区”、”町”、”村”が省略されている場合の補完。面倒そうなので無視。
・通り名などの対応
 →京都の通りなどのルールを知らないので、無視。
・ランドマーク検索
 →ポイントデータを簡単に得られる気配が無ければ、作る気なし。
・郵便番号検索
 →郵便番号データベースの住所を緯度経度に加工するのが面倒そうだけど、せっかくデータベースにも入れてあるし、やる価値はあるかな(?)
・事業者名検索
 →郵便番号データの事業者を拾って作れそうだけど面倒
・公共施設名検索
 →国土数値情報のポイントデータを拾って作れそうだけど面倒
・駅名検索
 →上記と同様、国土数値情報のポイントデータを拾って作れそうだけど面倒

※大体が、街区レベル位置参照情報の情報が最新じゃないってのがアウトかも。

*1:address == null) || (address.Length == 0

*2:instring[i].Length != 0) && (outstring[i].Length != 0

*3:address == null) ||((checkSQLInjection(address