Sequel で GeoHash 検索の実験をするために神奈川県の街区&大字レベル位置参照情報を SQLite3 に入れたらマッチングに苦労した

GeoHash を用いてプレインな RDBMS で点情報の検索をしてみたいと思い、街区&大字レベル位置参照情報を SQLite3 に入れようとしています。Mac & Ruby 縛りでやっています。コードの一致を意識していたら、いろいろと落とし穴にはまったので、このエントリでは、データベースに神奈川県のデータを入れるところで終わっています。
http://togetter.com/li/44975 などでまとめられているように、点情報の空間検索で GeoHash を使い、RDBMS の空間拡張を使用しないという話があります。RDBMS の空間拡張には「空間インデクスを使用した検索」と「空間拡張したSQL関数による幾何処理」の2つが期待されますが、GeoHash 検索によって、前者の代替が行えることになります。

私もこの技術を実験してみようと思い、街区レベル位置参照情報と大字・町丁目レベル位置参照情報( http://nlftp.mlit.go.jp/isj/ )を SQLite3 に格納してみました。

GeoHash という「コード」によって検索するのだなあと意識しすぎて、大字・町丁目レベル位置参照情報の「大字町丁目コード」が持つ魅力にとりつかれたため、街区レベル位置参照情報と大字・町丁目レベル位置参照情報とのマッチングにはまってしまいました。

コード

# -*- coding: utf-8 -*-
# load.rb
$KCODE = 'u'

require 'iconv'
require 'rubygems'
require 'sequel'
require 'pr_geohash'
require 'zip/zipfilesystem'

DB = Sequel::connect('sqlite://14.sqlite')

DB.create_table!(:prefs) do
  String :code, :unique => true
  String :name
end

DB.create_table!(:cities) do
  String :code, :unique => true
  String :name
end

DB.create_table!(:azachos) do
  String :code, :unique => true
  String :name
  String :geohash
end

DB.create_table!(:gaikus) do
  String :code
  String :fugo
  String :geohash
end

s2u = Iconv.new('UTF-8', 'Shift_JIS')

class Array
  def cache(value)
    self.shift
    self.push(value)
  end
end

pref_codes_inserted = [nil, nil, nil]
city_codes_inserted = [nil, nil, nil]

# 神奈川県の大字・町丁目レベル位置参照情報をロード
Zip::ZipFile.open('src/14000-03.0b.zip') {|zip|
  p zip.file.size('14_2009.csv')
  first = true
  zip.file.foreach('14_2009.csv') {|l|
    if first
      print s2u.iconv(l)
      first = false
      next
    end
    (pref_code, pref_name, city_code, city_name, azacho_code, azacho_name, ido, keido) = s2u.iconv(l).gsub('"', '').strip.split(',')
    city_name.sub!(//, '')
    azacho_name.sub!(//, '')
    unless pref_codes_inserted.include?(pref_code)
      DB[:prefs] << {:code => pref_code, :name => pref_name} if DB[:prefs].filter(:code => pref_code).count == 0
      pref_codes_inserted.cache(pref_code)
    end
    unless city_codes_inserted.include?(city_code)
      DB[:cities] << {:code => city_code, :name => city_name} if DB[:cities].filter(:code => city_code).count == 0
      city_codes_inserted.cache(city_code)
    end
    DB[:azachos] << {:code => azacho_code, :name => azacho_name, :geohash => GeoHash::encode(ido.to_f, keido.to_f, 12)} if DB[:azachos].filter(:code => azacho_code).count == 0
  }
}

# 神奈川県の街区レベル位置参照情報をロード
Zip::ZipFile.open('src/14000-08.0a.zip') {|zip|
  p zip.file.size('14_2009.csv')
  first = true
  zip.file.foreach('14_2009.csv') {|l|
    if first
      print s2u.iconv(l)
      first = false
      next
    end
    (pref_name, city_name, azacho_name, fugo, kei, x, y, ido, keido) = s2u.iconv(l).gsub('"', '').strip.split(',')
    city_name.sub!(//, '')
    azacho_name.sub!(//, '')
    city_name = $2.to_s if city_name.match(/(.*)(.*(|))/)
    pref_code = DB[:prefs].filter(:name => pref_name).first[:code]
    begin
      city_code = DB[:cities].filter(:name => city_name).filter(:code.like("#{pref_code}%")).first[:code]
    rescue
      p city_name
    end

    azacho_ds = DB[:azachos].filter(:name => azacho_name).filter(:code.like("#{city_code}%"))
    if azacho_ds.count == 1
      azacho_code = azacho_ds.first[:code]
      DB[:gaikus] << {:code => azacho_code, :fugo => fugo, :geohash => GeoHash::encode(ido.to_f, keido.to_f, 12)}
    else
      print "azacho_code for #{azacho_name} (#{pref_name}#{city_name}) not found.\n"
    end
  }
}

DB.disconnect

ポイント

  • 大字・町丁目レベル位置参照情報の市区町村名には郡名はなく、街区レベル位置参照情報には郡名がはいっているので、さっくりと正規表現を使って郡名を取り除こうとしています。しかし、この正規表現、町村名に「郡」と入っているときにうまく動いてくれない気がします。
  • 大字・町丁目レベル位置参照情報では「保土ケ谷区」ですが、街区レベル位置参照情報では「保土ヶ谷区」です。 http://ja.wikipedia.org/wiki/保土ケ谷区 を参考に、「保土ケ谷区」に合わせる場当たり的な処理を入れています。茅ヶ崎については「ヶ」が正しいのですが、大字・町丁目にあわせてとりあえず「茅ケ崎」に揃えています。その他、市区町村名についても大字・町丁目名についても、とりあえず「ヶ」をすべて「ケ」に置き換えています。
  • その他、町又は字の名前のレベルでのミスマッチが発生しています(例:棚沢 vs 棚澤)が、これについてはあとで考えることにします。
  • 処理の効率性をあまり考えていません。ユニークであるべきレコードを入れるときに、いちいちデータベースを検索しています。