fias 0.0.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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