fias 0.0.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -22
  3. data/.rubocop.yml +7 -0
  4. data/.travis.yml +10 -0
  5. data/Gemfile +1 -1
  6. data/LICENSE.txt +2 -2
  7. data/README.md +259 -155
  8. data/Rakefile +6 -1
  9. data/config/names.txt +0 -0
  10. data/config/synonyms.yml +50 -0
  11. data/examples/create.rb +106 -0
  12. data/examples/generate_index.rb +63 -0
  13. data/fias.gemspec +33 -21
  14. data/lib/fias.rb +197 -10
  15. data/lib/fias/config.rb +74 -0
  16. data/lib/fias/import/copy.rb +62 -0
  17. data/lib/fias/import/dbf.rb +81 -0
  18. data/lib/fias/import/download_service.rb +37 -0
  19. data/lib/fias/import/restore_parent_id.rb +51 -0
  20. data/lib/fias/import/tables.rb +74 -0
  21. data/lib/fias/name/append.rb +30 -0
  22. data/lib/fias/name/canonical.rb +42 -0
  23. data/lib/fias/name/extract.rb +85 -0
  24. data/lib/fias/name/house_number.rb +71 -0
  25. data/lib/fias/name/split.rb +60 -0
  26. data/lib/fias/name/synonyms.rb +93 -0
  27. data/lib/fias/query.rb +43 -0
  28. data/lib/fias/query/estimate.rb +67 -0
  29. data/lib/fias/query/finder.rb +75 -0
  30. data/lib/fias/query/params.rb +101 -0
  31. data/lib/fias/railtie.rb +3 -17
  32. data/lib/fias/version.rb +1 -1
  33. data/spec/fixtures/ACTSTAT.DBF +0 -0
  34. data/spec/fixtures/NORDOC99.DBF +0 -0
  35. data/spec/fixtures/STRSTAT.DBF +0 -0
  36. data/spec/fixtures/addressing.yml +93 -0
  37. data/spec/fixtures/query.yml +79 -0
  38. data/spec/fixtures/query_sanitization.yml +75 -0
  39. data/spec/fixtures/status_append.yml +60 -0
  40. data/spec/lib/import/copy_spec.rb +44 -0
  41. data/spec/lib/import/dbf_spec.rb +28 -0
  42. data/spec/lib/import/download_service_spec.rb +15 -0
  43. data/spec/lib/import/restore_parent_id_spec.rb +34 -0
  44. data/spec/lib/import/tables_spec.rb +26 -0
  45. data/spec/lib/name/append_spec.rb +14 -0
  46. data/spec/lib/name/canonical_spec.rb +20 -0
  47. data/spec/lib/name/extract_spec.rb +67 -0
  48. data/spec/lib/name/house_number_spec.rb +45 -0
  49. data/spec/lib/name/query_spec.rb +21 -0
  50. data/spec/lib/name/split_spec.rb +15 -0
  51. data/spec/lib/name/synonyms_spec.rb +51 -0
  52. data/spec/lib/query/params_spec.rb +15 -0
  53. data/spec/lib/query_spec.rb +27 -0
  54. data/spec/spec_helper.rb +30 -0
  55. data/spec/support/db.rb +30 -0
  56. data/spec/support/query.rb +13 -0
  57. data/tasks/db.rake +52 -0
  58. data/tasks/download.rake +15 -0
  59. metadata +246 -64
  60. data/lib/fias/active_record/address_object.rb +0 -231
  61. data/lib/fias/active_record/address_object_type.rb +0 -15
  62. data/lib/fias/dbf_wrapper.rb +0 -90
  63. data/lib/fias/importer.rb +0 -30
  64. data/lib/fias/importer/base.rb +0 -59
  65. data/lib/fias/importer/pg.rb +0 -81
  66. data/lib/fias/importer/sqlite.rb +0 -38
  67. data/lib/generators/fias/migration.rb +0 -34
  68. data/lib/generators/fias/templates/create_fias_tables.rb +0 -5
  69. data/tasks/fias.rake +0 -68
@@ -1,231 +0,0 @@
1
- # encoding: utf-8
2
- # TODO: Поделить на файлы
3
- module Fias
4
- class AddressObject < ActiveRecord::Base
5
- # TODO: Тут надо понять как префикс передать
6
- if defined?(Rails)
7
- self.table_name = "#{Rails.application.config.fias.prefix}_address_objects"
8
- else
9
- self.table_name = "fias_address_objects"
10
- end
11
-
12
- self.primary_key = 'aoid'
13
-
14
- alias_attribute :name, :formalname
15
- alias_attribute :id, :aoid
16
-
17
- # Родительские объекты (Ленобласть для Лодейнопольского района)
18
- # Для "проезд 1-й Конной Лахты 2-й" - Санкт-Петербург и Ленинград.
19
- # Блядь, 1-й проезд 2-й Конной Лахты, ебануться!
20
- # http://maps.yandex.ru/?text=%D0%A0%D0%BE%D1%81%D1%81%D0%B8%D1%8F%2C%20%D0%A1%D0%B0%D0%BD%D0%BA%D1%82-%D0%9F%D0%B5%D1%82%D0%B5%D1%80%D0%B1%D1%83%D1%80%D0%B3%2C%202-%D0%B9%20%D0%BF%D1%80%D0%BE%D0%B5%D0%B7%D0%B4%201-%D0%B9%20%D0%9A%D0%BE%D0%BD%D0%BD%D0%BE%D0%B9%20%D0%9B%D0%B0%D1%85%D1%82%D1%8B&sll=30.123296%2C60.007056&ll=30.123848%2C60.007475&spn=0.010085%2C0.006017&z=17&l=map
21
- has_many :parents,
22
- class_name: 'AddressObject',
23
- foreign_key: 'aoguid',
24
- primary_key: 'parentguid'
25
-
26
- # Дочерние объекты (например, улицы для города)
27
- has_many :children,
28
- class_name: 'AddressObject',
29
- primary_key: 'aoguid',
30
- foreign_key: 'parentguid'
31
-
32
- # Предыдущие исторические версии названия (Ленинград для Питера)
33
- # Может быть несколько, если произошло слияние
34
- has_many :previous_versions,
35
- class_name: 'AddressObject',
36
- foreign_key: 'aoid',
37
- primary_key: 'previd'
38
-
39
- # Следующая исторические версии названия (Питер для Ленинграда)
40
- # Может быть несколько, если произошло разделение
41
- has_many :next_versions,
42
- class_name: 'AddressObject',
43
- foreign_key: 'aoid',
44
- primary_key: 'nextid'
45
-
46
- # Полное наименование типа объекта (город, улица)
47
- belongs_to :address_object_type,
48
- class_name: '::Fias::AddressObjectType',
49
- foreign_key: 'shortname',
50
- primary_key: 'scname'
51
-
52
- # Актуальные записи (активные в настоящий момент)
53
- # Проверено, что livestatus уже достаточен для идентификации
54
- # актуальных объектов, вопреки показаниям вики.
55
- scope :actual, where(livestatus: 1)
56
-
57
- # Выбирает объекты определенного уровня, аргументы - символы из хеша
58
- # AOLEVELS
59
- scope :leveled, ->(*levels) {
60
- levels = Array.wrap(levels).map { |level| AOLEVELS[level] }
61
- where(aolevel: levels)
62
- }
63
-
64
- scope :sorted, order('formalname ASC')
65
- scope :with_types, includes(:address_object_type)
66
- scope :matching, ->(name) {
67
- scope = if self.connection.adapter_name
68
- where(%{ "formalname" @@ ? OR ? @@ "formalname"}, name, name)
69
- else
70
- where('formalname LIKE ?', "%#{name}%")
71
- end
72
-
73
- # Первыми идут центральные и крупные поселения
74
- scope.order('aolevel ASC, centstatus DESC')
75
- }
76
-
77
- scope :same_region, ->(address_object) {
78
- where(regioncode: address_object.regioncode)
79
- }
80
-
81
- # Значимые поселения
82
- scope :central, where('centstatus > 0')
83
-
84
- def has_history?
85
- previd.present?
86
- end
87
-
88
- def actual?
89
- livestatus == 1
90
- end
91
-
92
- # Актуальный родитель. Для 1-го проезда 2-й Конной Лахты - только Питер.
93
- def parent
94
- parents.actual.first
95
- end
96
-
97
- def aolevel_sym
98
- AOLEVELS.key(aolevel)
99
- end
100
-
101
- # Название с сокращением / полным наименованием города
102
- # "г. Санкт-Петербург" / "город Санкт-Петербург"
103
- #
104
- # Для работы метода требуется загрузить таблицу :address_object_types
105
- #
106
- # Параметр - режим:
107
- # :obviously - очевидный режим". В этом режиме "Дагестан" вместо
108
- # "Республика Дагестан", "АО" вместо "Автономная область",
109
- # но всегда "Краснодарский край"
110
- # :short - дописываются "г." и "респ."
111
- # :long - дописываются "город" и "Республика"
112
- #
113
- # Пример:
114
- # .abbrevated(:obviously) # Тульская область, Хакасия, Еврейская Аобл.
115
- # .abbrevated(:short) # Тульская область, Респ. Хакасия, Еврейская Аобл.
116
- # .abbrevated(:long) # Тульская область, Республика Хакасия, Еврейская автономная область
117
- def abbrevated(mode = :obviously)
118
- return name if address_object_type.blank? || shortname.blank?
119
-
120
- case aolevel_sym
121
- when :region
122
- # "Ханты-Мансийский Автономный округ - Югра"
123
- return name if regioncode == '86'
124
-
125
- ending = name[-2..-1]
126
-
127
- # Если название кончается на -ая -ий - по правилам русского языка
128
- # нужно дописать в конец "область", "край"
129
- must_append = SHN_MUST_APPEND_TO_ENDINGS.include?(ending) ||
130
- shortname == 'Чувашия'
131
-
132
- must_abbrevate = must_append ||
133
- shortname == 'Чувашия' ||
134
- mode != :obviously
135
-
136
- return name unless must_abbrevate
137
-
138
- abbr = case mode
139
- when :short
140
- shortname
141
- when :long
142
- address_object_type.name
143
- when :obviously
144
- if SHN_LEAVE_SHORTS_INTACT.include?(shortname)
145
- shortname
146
- else
147
- address_object_type.name
148
- end
149
- end
150
-
151
- # Точка не ставится для АО, края, Чувашии и длинных названий
152
- abbr = "#{abbr}." if mode == :short && not(shortname.in?(SHN_MUST_NOT_APPEND_DOT))
153
-
154
- if not(must_append) && must_abbrevate
155
- "#{abbr} #{name}"
156
- else
157
- # "Республика" => "республика", но "АО" остается
158
- abbr = abbr.mb_chars.downcase if not(shortname.in?(SHN_LEAVE_SHORTS_INTACT))
159
- "#{name} #{abbr}"
160
- end
161
- else
162
- abbr = if mode == :long
163
- address_object_type.try(:name)
164
- else
165
- shortname
166
- end
167
- "#{abbr} #{name}"
168
- end
169
- end
170
-
171
- class << self
172
- # Подробное описание см. в README
173
- # TODO: OPERSTATUS
174
- def match_existing(scope, fias_key_accessor, title_field_accessor, &block)
175
- scope.find_each do |record|
176
- aoid = record.send(fias_key_accessor)
177
- title = record.send(title_field_accessor)
178
-
179
- if aoid.present?
180
- match = scoped.find_by_aoid(aoid)
181
-
182
- if match.present?
183
- unless match.actual?
184
- next_versions = match.next_versions
185
-
186
- if next_versions.empty?
187
- yield(:deleted, record, match)
188
- elsif next_versions.count == 1
189
- next_version = next_versions.first
190
- previous_versions_of_current = next_version.previous_versions
191
-
192
- if previous_versions_of_current.count == 1
193
- yield(:updated, record, next_version)
194
- else
195
- yield(:joined, record, next_version, previous_versions_of_current)
196
- end
197
- elsif next_versions.count > 1
198
- yield(:split, record, next_versions)
199
- end
200
- end
201
- end
202
- else
203
- matches = scoped.matching(title)
204
- yield(:match, record, *matches)
205
- end
206
- end
207
- end
208
-
209
- def match_missing(scope, address_object_key_field, &block)
210
- scoped.each do |address_object|
211
- unless scope.where(address_object_key_field => address_object.aoid).exists?
212
- yield(:created, nil, address_object)
213
- end
214
- end
215
- end
216
- end
217
-
218
- # Коды уровня адресного объекта
219
- AOLEVELS = {
220
- region: 1, autonomy: 2, district: 3, city: 4,
221
- territory: 5, settlement: 6, street: 7,
222
- additional_territory: 90, additional_territory_slave: 91
223
- }
224
-
225
- # Дописывать сокращения обязательно, "Самарская" выглядит странно,
226
- # всегда должно быть "Самарская область", а "Дагестан" понятно и так.
227
- SHN_MUST_APPEND_TO_ENDINGS = %w(ая ий)
228
- SHN_MUST_NOT_APPEND_DOT = %w(край АО Чувашия) # Не дописывать точку к сокращению
229
- SHN_LEAVE_SHORTS_INTACT = %w(АО Аобл Чувашия) # В очевидном режиме не разворачивать "АО" в "Автономная область"
230
- end
231
- end
@@ -1,15 +0,0 @@
1
- # encoding: utf-8
2
- module Fias
3
- class AddressObjectType < ActiveRecord::Base
4
- # TODO: Тут надо понять как префикс передать
5
- if defined?(Rails)
6
- self.table_name = "#{Rails.application.config.fias.prefix}_address_object_types"
7
- else
8
- self.table_name = "fias_address_object_types"
9
- end
10
- self.primary_key = 'scname'
11
-
12
- alias_attribute :name, :socrname
13
- alias_attribute :abbrevation, :scname
14
- end
15
- end
@@ -1,90 +0,0 @@
1
- module Fias
2
- # Класс для доступа к DBF-файлам ФИАС.
3
- #
4
- # Пример:
5
- # wrapper = Fias::DbfWrapper.new('tmp/fias')
6
- # wrapper.address_objects.record_count
7
- # wrapper.address_objects.each { |record| record.attributes }
8
- #
9
- # TODO: Добавить в инишилайзер tables, чтобы при создании проверять
10
- # их наличие на диске
11
- class DbfWrapper
12
- # Открывает DBF-файлы ФИАСа
13
- def initialize(pathspec)
14
- unless Dir.exists?(pathspec)
15
- raise ArgumentError, 'FIAS database path does not exists'
16
- end
17
- self.pathspec = pathspec
18
-
19
- DBF_ACCESSORS.each do |accessor, dbf_name|
20
- filename = File.join(pathspec, dbf_name)
21
-
22
- if File.exists?(filename)
23
- dbf = DBF::Table.new(filename, nil, DEFAULT_ENCODING)
24
- send("#{accessor}=", dbf)
25
- end
26
- end
27
- end
28
-
29
- # Возвращает хеш {аксессор dbf-таблицы => таблица}
30
- # На входе массив названий таблиц (строки), без параметров - все
31
- # Макрос houses вернет все таблицы домов
32
- #
33
- # Пример:
34
- # wrapper = Fias::DbfWrapper.new('tmp/fias')
35
- # wrapper.tables(%w(address_objects address_object_types houses))
36
- # ... {
37
- # ... address_objects: Dbf::Table(...),
38
- # ... address_object_types: Dbf::Table(...),
39
- # ... house1: Dbf::Table(...),
40
- # ... ...
41
- # ... house99: Dbf::Table(...)
42
- # ... }
43
- def tables(*only)
44
- only = only.first if only.first.is_a?(Array)
45
- only = only.map(&:to_s)
46
-
47
- hash = DBF_ACCESSORS.keys.map do |accessor|
48
- accessor_s = accessor.to_s
49
- is_houses = only.include?('houses') && accessor_s.starts_with?('house')
50
-
51
- if only.include?(accessor_s) || is_houses
52
- [accessor, send(accessor)]
53
- end
54
- end
55
- Hash[*hash.compact.flatten]
56
- end
57
-
58
- # { house01: "HOUSE01"..house99: "HOUSE99" }
59
- HOUSES_ACCESSORS = Hash[*(1..99).map { |n|
60
- [("house%0.2d" % n).to_sym, "HOUSE%0.2d.dbf" % n]
61
- }.flatten]
62
-
63
- # Таблица соответствий аттрибутов класса DBF-файлам
64
- DBF_ACCESSORS = {
65
- address_object_types: 'SOCRBASE.DBF',
66
- current_statuses: 'CURENTST.DBF',
67
- actual_statuses: 'ACTSTAT.DBF',
68
- operation_statuses: 'OPERSTAT.DBF',
69
- center_statuses: 'CENTERST.DBF',
70
- interval_statuses: 'INTVSTAT.DBF',
71
- estate_statues: 'ESTSTAT.DBF',
72
- structure_statuses: 'STRSTAT.DBF',
73
- address_objects: 'ADDROBJ.DBF',
74
- house_intervals: 'HOUSEINT.DBF',
75
- landmarks: 'LANDMARK.DBF'
76
- }.merge(
77
- HOUSES_ACCESSORS
78
- )
79
-
80
- DEFAULT_ENCODING = Encoding::CP866
81
-
82
- attr_reader *DBF_ACCESSORS.keys
83
- attr_reader :houses
84
-
85
- private
86
- attr_accessor :pathspec
87
- attr_writer *DBF_ACCESSORS.keys
88
- attr_writer :houses
89
- end
90
- end
@@ -1,30 +0,0 @@
1
- module Fias
2
- module Importer
3
- # Фактори-метод, возвращает ссылку на объект импортера
4
- # Принимает параметры:
5
- # :adapter - название адаптера
6
- # :connection - прямое соединение с базой данных (connection.raw_connection)
7
- def self.build(options = {})
8
- adapter = options.delete(:adapter) ||
9
- ActiveRecord::Base.connection_config[:adapter]
10
-
11
- connection = options.delete(:connection) ||
12
- ActiveRecord::Base.connection.raw_connection
13
-
14
- case adapter
15
- when 'postgresql'
16
- Pg.new(
17
- connection,
18
- options
19
- )
20
- when 'sqlite3'
21
- Sqlite.new(
22
- connection,
23
- options
24
- )
25
- else
26
- raise 'Only postgres & sqlite supported now, fork'
27
- end
28
- end
29
- end
30
- end
@@ -1,59 +0,0 @@
1
- module Fias
2
- module Importer
3
- class Base
4
- def initialize(connection, options = {})
5
- self.prefix = options.delete(:prefix) || 'fias'
6
- self.connection = connection
7
- end
8
-
9
- # На входе - хеш (имя таблицы => dbf)
10
- def schema(tables)
11
- "".tap do |s|
12
- tables.each do |name, dbf|
13
- if dbf.present?
14
- s << schema_for(name, table_name(name), dbf)
15
- end
16
- end
17
- end
18
- end
19
-
20
- def schema_for(name, table_name, dbf)
21
- "".tap do |s|
22
- s << %{create_table "#{table_name}", id: false do |t|\n}
23
- s << schema_columns(name, dbf)
24
- s << "end\n"
25
- end
26
- end
27
-
28
- def import(tables, &block)
29
- tables.each do |name, dbf|
30
- if dbf
31
- import_table(name, table_name(name), dbf, &block)
32
- end
33
- end
34
- end
35
-
36
- def import_table(name, table_name, dbf, &block)
37
- raise NotImplementedError, 'Implement this in concrete class'
38
- end
39
-
40
- protected
41
- attr_accessor :prefix, :connection
42
-
43
- private
44
- def table_name(name)
45
- "#{prefix}_#{name}"
46
- end
47
-
48
- def schema_columns(accessor, table)
49
- "".tap do |s|
50
- table.columns.each do |column|
51
- column_name = column.name.downcase
52
- column_def = column.schema_definition
53
- s << " t.column #{column_def}"
54
- end
55
- end
56
- end
57
- end
58
- end
59
- end
@@ -1,81 +0,0 @@
1
- module Fias
2
- module Importer
3
- # Класс для импорта данных из ФИАС в PostgreSQL.
4
- # Используется COPY FROM STDIN.
5
- class Pg < Base
6
- def schema_for(name, table_name, dbf)
7
- super + alter_table_to_pg_uuids(name, table_name)
8
- end
9
-
10
- private
11
- def alter_table_to_pg_uuids(name, table_name)
12
- "".tap do |s|
13
- columns = CONVERT_TO_UUID[name]
14
- if columns.present?
15
- columns.each do |column|
16
- s << %{ActiveRecord::Base.connection.execute("ALTER TABLE #{table_name} ALTER COLUMN #{column} TYPE UUID USING CAST (#{column} AS UUID);")\n}
17
- end
18
- end
19
- end
20
- end
21
-
22
- def import_table(name, table_name, dbf, &block)
23
- fields = table_fields(dbf)
24
- columns = table_columns(fields)
25
-
26
- truncate_table(table_name)
27
- copy_from_stdin(table_name, columns)
28
-
29
- dbf.each_with_index do |record, index|
30
- should_import = yield(name, record.attributes, index) if block_given?
31
-
32
- unless should_import === false
33
- data = record.to_a
34
- put_data(data)
35
- end
36
- end
37
-
38
- put_copy_end
39
- end
40
-
41
- def truncate_table(table_name)
42
- connection.exec "TRUNCATE TABLE #{table_name};"
43
- end
44
-
45
- def table_fields(table)
46
- table.columns.map(&:name).map(&:downcase)
47
- end
48
-
49
- def table_columns(fields)
50
- fields.join(', ')
51
- end
52
-
53
- def copy_from_stdin(table_name, columns)
54
- sql = "COPY #{table_name} (#{columns}) FROM STDIN NULL AS '-nil-'\n"
55
- connection.exec(sql)
56
- end
57
-
58
- def put_data(data)
59
- data.map! { |item| item == "" ? '-nil-' : item }
60
- line = data.join("\t") + "\n"
61
- connection.put_copy_data(line)
62
- end
63
-
64
- def put_copy_end
65
- connection.put_copy_end
66
-
67
- while res = connection.get_result
68
- result_status = res.res_status(res.result_status)
69
- unless result_status == 'PGRES_COMMAND_OK'
70
- raise "Import failure: #{result_status}"
71
- end
72
- end
73
- end
74
- end
75
-
76
- # Эти поля нужно отконвертировать в тип UUID после создания
77
- CONVERT_TO_UUID = {
78
- address_objects: %w(aoguid aoid previd nextid parentguid)
79
- }
80
- end
81
- end