Достаточно часто при работе с вводимыми пользователем адресными данными возникает необходимость сначала подсказать ему, как правильно свой адрес указать, а потом - полученную строку адреса привести в некоторый машинно-читаемый вид.
Таким видом у нас в стране можно считать код по справочникам КЛАДР или ФИАС.
Первый из них уже несколько лет считается устаревающим, но отличается более простой структурой и исторически продолжает использоваться во множестве систем, поскольку вполне подходит для большинства задач.
Давайте научимся разбирать строку адреса "туда и обратно", а заодно познакомимся с некоторыми алгоритмическими подходами и их реализацией на SQL.

Получение справочника КЛАДР
База КЛАДР в настоящее время администрируется ФНС и представлена на сайте ГНИВЦ в виде периодически (примерно раз в неделю) обновляемого архива. Для начала мы научимся его скачивать, исправлять некоторые ошибки и преобразовывать в более подходящую для наших задач структуру.
Для этого нам понадобятся wget, p7zip, dbview, recode и psql.
Исходный архив
База выкладывается на сервере ГНИВЦ всегда по одному и тому же адресу в виде 7z-архива Base.7z - версии отличаются друг от друга только заголовком Last-Modified в ответе сервера.
При этом сервер достаточно нервно реагирует на скорость загрузки и не поддерживает HEAD-запросы, увы. Поэтому нам придется скачивать архив ежесуточно полностью, но чтобы снизить нагрузку на сервер установим ограничение скорости, а заголовки ответа сохраним и разберем отдельно:
wget -S https://gnivc.ru/html/gnivcsoft/KLADR/Base.7z --limit-rate=8k 2>.hdr
В сгенерированном .hdr нас будет интересовать эта строка:
... Last-Modified: Thu, 05 Aug 2021 09:28:39 GMT ...
Чтобы не пытаться обрабатывать архив и обновлять данные в нашей базе повторно, будем сохранять в ней этот таймстамп для последующих сравнений.
Если же таймстамп файла не совпал с сохраненным, распакуем полученный архив:
p7zip -d Base.7z
Результатом будет 7 DBF-файлов в DOS-кодировке:
Extracting ALTNAMES.DBF Extracting DOMA.DBF Extracting FLAT.DBF Extracting KLADR.DBF Extracting NAMEMAP.DBF Extracting SOCRBASE.DBF Extracting STREET.DBF
Переберем все эти файлы, формируя единый скрипт выгрузки данных через psql в COPY-формате:
# получаем структуру полей DBF # | DOS2WIN # | берем только описания полей (skip 2 строки) # | оставляем только их имена dbview -b -t -e -o -r ALTNAMES.DBF \ | recode CP866..CP1251 \ | tail -n+2 \ | xargs -l \ | egrep -io "^[a-z0-9_]+" # ... формируем SQL-заголовок временной таблицы # получаем данные DBF, разделенные '~' # | склеиваем "висящие" строки ([\t\r\n] в теле поля данных) # | DOS2WIN # | убираем все '\t', убираем концевые '~', заменяем '~'->'\t' dbview -d~ -b -t ALTNAMES.DBF \ | sed -e :a -e '/[\r\t]$/N; s/[\r\t]\n//g; ta' \ | recode CP866..CP1251 \ | sed -e 's/\t//g; s/~\r//g; s/~,/,/g; s/~/\t/g' >>.sql
По итогу мы получаем большой-большой SQL-файл примерно такого вида:
CREATE TEMPORARY TABLE "STREET.DBF"( "NAME" varchar, "SOCR" varchar, "CODE" varchar, "INDEX" varchar, "GNINMB" varchar, "UNO" varchar, "OCATD" varchar ); COPY "STREET.DBF"( "NAME", "SOCR", "CODE", "INDEX", "GNINMB", "UNO", "OCATD" ) FROM stdin; Абадзехская ул 01000001000000100 385013 0105 79401000000 Абрикосовая ул 01000001000000200 385013 0105 79401000000 Авиационный пер 01000001000000300 385006 0105 79401000000 Автодорога 7 ул 01000001000000400 385019 0105 79401000000 ...
Импорт данных
Поскольку сами данные в таблицах архива представлены в DOS-кодировке, но это как-то совсем уж немодно, развернем нашу базу в WIN1251 - она тоже однобайтовая, поэтому все поиски данных не станут тяжелее:
CREATE DATABASE kladr WITH ENCODING='WIN1251' OWNER=postgres TEMPLATE=template0 LC_COLLATE='ru_RU.CP1251' LC_CTYPE='ru_RU.CP1251' CONNECTION LIMIT=-1;
-- лог проверок обновлений CREATE TABLE kladr_chk( id serial PRIMARY KEY, ts timestamp DEFAULT now(), hostname varchar ); -- лог проведенных обновлений CREATE TABLE kladr_upd( id serial PRIMARY KEY, ts timestamp DEFAULT now(), lm varchar, hostname varchar );
Защита от параллельной загрузки
Поскольку импорт данных из ГНИВЦ у нас может проходить длительное время и инициироваться с разных источников (мало ли, один из инстансов прикажет долго жить), неплохо бы позаботиться о защите данных, чтобы они не пострадали - дописываем все это в начале .sql:
-- защита от автоотключения по таймауту SET statement_timeout = 0; -- включаем WIN-кодировку SET client_encoding = 'WIN1251'; -- включаем application_name для мониторинга активного процесса SET application_name = 'kladr : import [`hostname`]'; -- включаем "последовательные" транзакции BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- блокируем эксклюзивно upd-таблицу для "замораживания" параллельных операций LOCK TABLE kladr_upd IN EXCLUSIVE MODE NOWAIT; -- сбрасываем информацию в таблицу протокола проверок INSERT INTO kladr_chk(hostname) VALUES('`hostname`'); -- сравниваем Last-Modified загруженного файла и состояния в базе SELECT lm FROM kladr_upd ORDER BY id DESC LIMIT 1; -- если lm не совпадает -- для каждого DBF CREATE TEMPORARY TABLE ...; COPY ... FROM stdin; ... \. -- -- обновляем "версию" нашей базы INSERT INTO kladr_upd(lm, hostname) VALUES('$lm', '`hostname`'); COMMIT;
В результате такого подхода мы всегда знаем, кто и когда проверял наличие обновлений, и кому это удалось.
Соберем все вместе в единый скрипт:
Много bash-кода
#!/bin/sh . `dirname "$0"`/../app.conf echo "`date '+%F %T'` ==== Connecting to DB : $pghost:$pgport:$pgbase:$pguser" # тестирование подключения к БД psql -t -c 'SELECT 1' -h $pghost -p $pgport -U $pguser -w $pgbase 1>/dev/null 2>/dev/null rv="$?" if [ "$rv" != "0" ]; then echo "$pghost:$pgport:$pgbase:$pguser:$pgpass" >>~/.pgpass chmod 0600 ~/.pgpass psql -t -c 'SELECT 1' -h $pghost -p $pgport -U $pguser -w $pgbase 1>/dev/null 2>/dev/null rv="$?" fi if [ "$rv" != "0" ]; then echo "DB not connected : $pghost:$pgport:$pgbase:$pguser" exit 1 fi # инициализация каталога _dbf #_dbf=`mktemp -d` mkdir ./dbf && chmod 777 ./dbf _dbf=`readlink -f ./dbf` rm -rf ${_dbf} 2>/dev/null mkdir ${_dbf} 2>/dev/null touch ${_dbf}/.sql dir=`dirname "$0"` dir=`readlink -f $dir` ## импорт базы КЛАДР'а в _dbf/.sql # защита о�� автоотключения по таймауту echo "SET statement_timeout = 0;" >>${_dbf}/.sql # включаем WIN-кодировку echo "SET client_encoding = 'WIN1251';" >>${_dbf}/.sql # включаем application_name для мониторинга активного процесса echo "SET application_name = 'kladr : import [`hostname`]';" >>${_dbf}/.sql # включаем "последовательные" транзакции echo "BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;" >>${_dbf}/.sql # блокируем эксклюзивно upd-таблицу для "замораживания" параллельных операций echo "LOCK TABLE kladr_upd IN EXCLUSIVE MODE NOWAIT;" >>${_dbf}/.sql # сбрасываем информацию в таблицу протокола проверок echo "INSERT INTO kladr_chk(hostname) VALUES('`hostname`');" >>${_dbf}/.sql # инициализация временного каталога импорта #tmp=`mktemp -d` mkdir ./tmp && chmod 777 ./tmp tmp=`readlink -f ./tmp` cd $tmp echo "`date '+%F %T'` ==== Downloading : $source" # загрузка базы КЛАДР'а с ограничением по скорости или без wget -S $source --limit-rate=8k 2>.hdr # wget -S $source 2>.hdr echo "`date '+%F %T'` ==== Comparing 'Last-Modified'" rc=`cat .hdr | egrep 'HTTP/[0-9]\.[0-9] [0-9]{3}' | sed -e 's/^[ ]*HTTP\/[0-9]\.[0-9][ ]*\([0-9]*\).*$/\1/i' | egrep -v '301' | head -1` lm=`cat .hdr | egrep 'Last-Modified' | sed -e 's/^[ ]*Last-Modified:[ ]*//i' | head -1` echo " -- HTTP code : $rc" echo " -- HTTP 'Last-Modified' : $lm" pglm=`psql -h $pghost -p $pgport -U $pguser -w -t -c 'SELECT lm FROM kladr_upd ORDER BY id DESC LIMIT 1' $pgbase | sed -e 's/^[ ]*//i'` echo " -- PGDB 'Last-Modified' : $pglm" if [ "$rc" = "200" ] && [ "$lm" != "" ] && [ "$lm" != "$pglm" ]; then # распаковка базы echo "`date '+%F %T'` ==== Unpacking 7z" p7zip -d Base.7z 1>/dev/null 2>/dev/null cp $tmp/* ${_dbf} cd $dir echo "`date '+%F %T'` ==== Processing DBF" # обработка всех .DBF for dbf in `find ${_dbf} -maxdepth 1 -iname '*.DBF'`; do dbfn=`basename $dbf | tr '[:lower:]' '[:upper:]'` # преобразование заголовков echo " -- DBF : $dbfn" echo " -- header" # получаем структуру полей DBF | DOS2WIN | берем только описания полей (skip 2 строки) | оставляем только их имена fld=`dbview -b -t -e -o -r $dbf | recode CP866..CP1251 | tail -n+2 | xargs -l | egrep -io "^[a-z0-9_]+"` echo "CREATE TEMPORARY TABLE \"$dbfn\"(" >>${_dbf}/.sql fl="0" for i in ${fld}; do [ "$fl" = "1" ] && echo ',' >>${_dbf}/.sql echo -n " \"$i\"\n varchar" >>${_dbf}/.sql fl="1" done echo ");" >>${_dbf}/.sql # преобразование данных echo " -- data" echo "COPY \"$dbfn\"(" >>${_dbf}/.sql fl="0" for i in ${fld}; do [ "$fl" = "1" ] && echo ',' >>${_dbf}/.sql echo -n " \"$i\"" >>${_dbf}/.sql fl="1" done echo ") FROM stdin;" >>${_dbf}/.sql # получаем данные DBF, разделенные '~' | склеиваем "висящие" строки ([\t\r\n] в теле поля данных) | DOS2WIN | убираем все '\t' | убираем концевые ';' | заменяем ';'->'\t' dbview -d~ -b -t $dbf | sed -e :a -e '/[\r\t]$/N; s/[\r\t]\n//g; ta' | recode CP866..CP1251 | sed -e 's/\t//g; s/~\r//g; s/~,/,/g; s/~/\t/g' >>${_dbf}/.sql echo "\\." >>${_dbf}/.sql done # интеграция процедуры обновления базы - последовательное подключение всех sql-файлов импорта ls ${dir}/import/*.sql | xargs -l readlink -f | xargs -l -I{} cat {} >>${_dbf}/.sql # вставка метки обновления echo "INSERT INTO kladr_upd(lm, hostname) VALUES('$lm', '`hostname`');" >>${_dbf}/.sql fi echo "COMMIT;" >>${_dbf}/.sql cd $dir rm -rf $tmp echo "`date '+%F %T'` ==== Processing SQL" psql -h $pghost -p $pgport -U $pguser -w -f ${_dbf}/.sql $pgbase rv="$?" if [ "$rv" = "0" ]; then rm -rf ${_dbf}/ 2>/dev/null fi echo "`date '+%F %T'` ==== Exit : $rv" exit "$rv"
Тут можно использовать для импорта и временные каталоги, создаваемые через mktemp, но хватит ли у вас места на tmp-разделе?..
Реквизиты доступа к базе и КЛАДР-источник в нашем случае будут храниться в app.conf:
pghost="kladr.tensor.ru" pgport="5432" pguser="postgres" pgpass="postgres" pgbase="kladr" source="https://gnivc.ru/html/gnivcsoft/KLADR/Base.7z"
Поисковая база
А зачем нам вообще нужна какая-то другая структура? Чем нас не устраивают таблицы в оригинальном КЛАДР-архиве?
хранение адресных объектов (улиц и населенных пунктов) в разных структурах
хранение статуса/версии объекта (00, 51, ...) вместе с его кодом
невозможность наложить эффективные для поиска индексы
Напомню, что оригинальный код КЛАДР, согласно документации имеет вид СС РРР ГГГ ППП УУУУ АА, где:
СС - код субъекта РФ (региона)
РРР - код района
ГГГ - код города
ППП - код населенного пункта
УУУУ - код улицы (отсутствует у населенных пунктов)
АА - признак актуальности
То есть если вынести признак актуальности в отдельное поле, то у кодов многих объектов (например, городов) в конце окажутся нули, которые стоит безболезненно отсечь. И тогда коды будут иметь строго ограниченный набор длин в соответствии с "уровнем" объекта:
2 - регионы и города федерального подчинения (
77- г. Москва,76- Ярославская обл.)5 - районы (
76 001- Ярославская обл., Ярославский р-н)8 - города (
76 000 001- Ярославская обл., г. Ярославль)11 - населенные пункты (
76 001 000 008- Ярославская обл., Ярославский р-н, д.Алешково)15 - улицы (
76 001 000 008 0001- Ярославская обл., Ярославский р-н, д.Алешково, ул.Малиновая)
При этом, как видим, коды "вышестоящих" объектов становятся префиксами кодов объектов вложенных.
Что же получилось по структуре?

-- основная таблица хранения адресных объектов CREATE TABLE kladr( code varchar, status varchar, name varchar, abbr varchar, idx varchar, ifns varchar, ocato varchar, lvl smallint, norm varchar, PRIMARY KEY(code, status) ); -- индекс по почтовому индексу CREATE INDEX "kladr-idx" ON kladr(idx); -- префиксный индекс по названию объекта CREATE INDEX "kladr-nm" ON kladr(length(code), code varchar_pattern_ops, lower(name)); -- дома, привязанные к адресным объектам CREATE TABLE kladr_house( code varchar, codeExt varchar, name varchar, idx varchar, ifns varchar, ocato varchar, PRIMARY KEY(code, codeExt) ); -- замены и объединения объектов CREATE TABLE kladr_repl( oldCode varchar, newCode varchar, PRIMARY KEY(oldCode, newCode) ); CREATE INDEX "kladr-repl-new" ON kladr_repl(newCode); CREATE INDEX "kladr-repl-old" ON kladr_repl(oldCode); -- аббревиатуры административно-территориальных единиц ("ул", "пер", "г") CREATE TABLE kladr_abbr( code varchar, lvl smallint, name varchar, PRIMARY KEY(code, lvl) ); -- индекс по возможному "уровню" объекта CREATE INDEX "kladr-abbr-lvl" ON kladr_abbr(lvl);
Это основные таблицы, данные в которые импортируются непосредственно из соответствующих DBF по модели наложения "диффов", описанной в статье "DBA: грамотно организовываем синхронизации и импорты":
kladr-abbr
--// КЛАДР : сокращения -- создаем временную таблицу с импортируемыми данными КЛАДР'а CREATE TEMPORARY TABLE _kladr_abbr( LIKE kladr_abbr INCLUDING INDEXES ); -- заполняем преобразованными из DBF данными INSERT INTO _kladr_abbr(code, lvl, name) SELECT "SCNAME", "KOD_T_ST"::smallint, "SOCRNAME" FROM "SOCRBASE.DBF"; -- удаляем отсутствующие DELETE FROM kladr_abbr T USING kladr_abbr X LEFT JOIN _kladr_abbr Y USING(code, lvl) WHERE (T.code, T.lvl) = (X.code, X.lvl) AND Y IS NULL; -- обновляем оставшиеся UPDATE kladr_abbr kl SET name = kli.name FROM _kladr_abbr kli WHERE (kl.code, kl.lvl) = (kli.code, kli.lvl) AND ( kl.name ) IS DISTINCT FROM ( kli.name ); -- очищаем совпадающие DELETE FROM _kladr_abbr kli USING kladr_abbr kl WHERE (kli.code, kli.lvl) = (kl.code, kl.lvl); -- вставляем оставшиеся INSERT INTO kladr_abbr SELECT * FROM _kladr_abbr;
kladr
--// КЛАДР : от регионов до улиц -- создаем временную таблицу с импортируемыми данными КЛАДР'а CREATE TEMPORARY TABLE _kladr( LIKE kladr INCLUDING INDEXES ); -- заполняем преобразованными из DBF данными INSERT INTO _kladr(code, status, name, abbr, idx, ifns, ocato, lvl) SELECT DISTINCT ON(code, status) * FROM ( SELECT regexp_replace(rpad(substr("CODE", 1, length("CODE") - 2), 15, '0'), '^(.{0,}?)((((0{3})?0{3})?0{3})?0{4})?$', E'\\1', 'ig') code, substr("CODE", length("CODE") - 1, 2) status, "NAME", "SOCR", nullif("INDEX", ''), nullif("GNINMB", ''), nullif("OCATD", ''), "STATUS" FROM ( SELECT "CODE", "NAME", "SOCR", "INDEX", "GNINMB", "OCATD", "STATUS"::smallint FROM "KLADR.DBF" UNION ALL SELECT "CODE", "NAME", "SOCR", "INDEX", "GNINMB", "OCATD", NULL::smallint "STATUS" FROM "STREET.DBF" ) T ) T; -- удаляем отсутствующие DELETE FROM kladr T USING kladr X LEFT JOIN _kladr Y USING(code, status) WHERE (T.code, T.status) = (X.code, X.status) AND Y IS NULL; -- обновляем оставшиеся UPDATE kladr kl SET ( name, abbr, idx, ifns, ocato, lvl ) = ( kli.name, kli.abbr, kli.idx, kli.ifns, kli.ocato, kli.lvl ) FROM _kladr kli WHERE (kl.code, kl.status) = (kli.code, kli.status) AND ( kl.name, kl.abbr, kl.idx, kl.ifns, kl.ocato, kl.lvl ) IS DISTINCT FROM ( kli.name, kli.abbr, kli.idx, kli.ifns, kli.ocato, kli.lvl ); -- очищаем совпадающие DELETE FROM _kladr kli USING kladr kl WHERE (kli.code, kli.status) = (kl.code, kl.status); -- вставляем оставшиеся INSERT INTO kladr SELECT * FROM _kladr; -- обновляем поисковый кэш DELETE FROM kladr_kw WHERE (code, status) IN ( SELECT (ro).code, (ro).status FROM kladr$log WHERE ro IS DISTINCT FROM NULL ); INSERT INTO kladr_kw(code, status, keyword) SELECT DISTINCT code, status, kw FROM ( SELECT (rn).code, (rn).status, regexp_split_to_table(lower((rn).name), E'[^\\-a-zа-яё0-9]+', 'i') kw FROM kladr$log WHERE rn IS DISTINCT FROM NULL ) T WHERE kw <> ''; DELETE FROM kladr$log;
Здесь регулярное выражение используется для отсечения "хвостовых" нулей по маске до необходимой нам длины. То есть нельзя просто так взять 76 000 010 000 и убрать все 4 последних ноля, поскольку 010 тут является значимым кодом города.
kladr-house
--// КЛАДР : дома -- создаем временную таблицу с импортируемыми данными КЛАДР'а CREATE TEMPORARY TABLE _kladr_house( LIKE kladr_house INCLUDING INDEXES ); -- заполняем преобразованными из DBF данными INSERT INTO _kladr_house(code, codeExt, name, idx, ifns, ocato) SELECT regexp_replace(substr("CODE", 1, 15), '^(.{0,}?)((((0{3})?0{3})?0{3})?0{4})?$', E'\\1', 'ig'), substr("CODE", 16, 4), "NAME", nullif("INDEX", ''), nullif("GNINMB", ''), nullif("OCATD", '') FROM "DOMA.DBF"; -- удаляем отсутствующие DELETE FROM kladr_house T USING kladr_house X LEFT JOIN _kladr_house Y USING(code, codeExt) WHERE (T.code, T.codeExt) = (X.code, X.codeExt) AND Y IS NULL; -- обновляем оставшиеся UPDATE kladr_house kl SET ( name, idx, ifns, ocato ) = ( kli.name, kli.idx, kli.ifns, kli.ocato ) FROM _kladr_house kli WHERE (kl.code, kl.codeExt) = (kli.code, kli.codeExt) AND ( kl.name, kl.idx, kl.ifns, kl.ocato ) IS DISTINCT FROM ( kli.name, kli.idx, kli.ifns, kli.ocato ); -- очищаем совпадающие DELETE FROM _kladr_house kli USING kladr_house kl WHERE (kli.code, kli.codeExt) = (kl.code, kl.codeExt); -- вставляем оставшиеся INSERT INTO kladr_house SELECT * FROM _kladr_house; -- обновляем поисковый кэш DELETE FROM kladr_hs WHERE (code) IN ( SELECT (ro).code FROM kladr_house$log WHERE ro IS DISTINCT FROM NULL UNION ALL SELECT (rn).code FROM kladr_house$log WHERE rn IS DISTINCT FROM NULL ); -- заполняем преобразованными данными CREATE TEMPORARY TABLE _kladr_hs0 AS SELECT DISTINCT ON(code, house) code, idx, ifns, ocato, unnest(houses) house FROM ( SELECT *, CASE WHEN _range IS NULL AND name ~ E'_' THEN ARRAY[regexp_replace(name, '_', '-')] WHEN _range IS NULL THEN ARRAY[name] WHEN _range IS NOT NULL THEN ARRAY( SELECT i::text FROM generate_series(_range[1]::integer + CASE WHEN _range[4] IS NOT NULL THEN (_range[1]::integer + _range[4]::integer) % 2 ELSE 0 END, _range[2]::integer, _range[3]::integer) i ) ELSE NULL END houses FROM ( SELECT code, idx, ifns, ocato, name, CASE WHEN name ~ E'^Н\\(\\d+-\\d+\\)$' THEN regexp_split_to_array(substr(name, 3, length(name) - 3), '-') || '2'::text || '1'::text WHEN name ~ E'^Ч\\(\\d+-\\d+\\)$' THEN regexp_split_to_array(substr(name, 3, length(name) - 3), '-') || '2'::text || '0'::text WHEN name = 'Н' THEN '{1,999,2}'::text[] WHEN name = 'Ч' THEN '{2,998,2}'::text[] WHEN name ~ E'^\\d+-\\d+$' THEN regexp_split_to_array(name, '-') || '1'::text ELSE NULL END _range FROM ( SELECT code, idx, ifns, ocato, unnest(regexp_split_to_array(upper(name), ',')) "name" FROM kladr_house WHERE (code) IN ( SELECT (ro).code FROM kladr_house$log WHERE ro IS DISTINCT FROM NULL UNION ALL SELECT (rn).code FROM kladr_house$log WHERE rn IS DISTINCT FROM NULL ) ) T ) T ) T ORDER BY code, house, (_range IS NULL) DESC; CREATE INDEX ON _kladr_hs0(code, house, idx DESC NULLS LAST); CREATE TEMPORARY TABLE _kladr_hs1 AS SELECT DISTINCT ON (code, house) code, idx, ifns, ocato, house FROM _kladr_hs0 ORDER BY code, house, idx DESC NULLS LAST; CREATE INDEX ON _kladr_hs1(code, house); CREATE TEMPORARY TABLE _kladr_hs2 AS SELECT code, coalesce( idx, coalesce( ( SELECT idx FROM _kladr_hs1 WHERE (code, house) = (T.code, regexp_replace(T.house, E'^(\\d+)(\\D)?.*$', E'\\1', 'ig')) LIMIT 1 ), coalesce( ( SELECT idx FROM kladr WHERE code IN ( substr(T.code, 1, 15), substr(T.code, 1, 11), substr(T.code, 1, 8), substr(T.code, 1, 5), substr(T.code, 1, 2) ) AND -- status = '00' AND idx IS NOT NULL ORDER BY length(code) DESC LIMIT 1 ), '' ) ) ) idx, ifns, ocato, house FROM _kladr_hs1 T; CREATE INDEX ON _kladr_hs2(code, idx, ifns, ocato, house); INSERT INTO kladr_hs(code, idx, ifns, ocato, houses) SELECT code, idx, ifns, ocato, array_agg(house ORDER BY house) houses FROM _kladr_hs2 GROUP BY 1, 2, 3, 4; DELETE FROM kladr_house$log;
Здесь регулярными выражениями мы приводим форматы исходной базы в списки конкретных номеров домов:
5-11 -> {5, 6, 7, 8, 9, 10, 11}Н(5-11) -> {5, 7, 9, 11}Ч(6-10) -> {6, 8, 10}Н -> {1, 3, ..., 999}Ч -> {2, 4, ..., 998}
kladr-repl
--// КЛАДР : замены -- создаем временную таблицу с импортируемыми данными КЛАДР'а CREATE TEMPORARY TABLE _kladr_repl( LIKE kladr_repl INCLUDING INDEXES ); -- заполняем преобразованными из DBF данными INSERT INTO _kladr_repl(oldCode, newCode) SELECT DISTINCT co, cn FROM ( SELECT regexp_replace(rpad(co, 15, '0'), '^(.{0,}?)((((0{3})?0{3})?0{3})?0{4})?$', E'\\1', 'ig') co, so, regexp_replace(rpad(cn, 15, '0'), '^(.{0,}?)((((0{3})?0{3})?0{3})?0{4})?$', E'\\1', 'ig') cn, sn FROM ( SELECT *, substr("OLDCODE", 1, length("OLDCODE") - 2) co, substr("OLDCODE", length("OLDCODE") - 1, 2) so, substr("NEWCODE", 1, length("NEWCODE") - 2) cn, substr("NEWCODE", length("NEWCODE") - 1, 2) sn FROM "ALTNAMES.DBF" ) T ) T; -- удаляем отсутствующие DELETE FROM kladr_repl T USING kladr_repl X LEFT JOIN _kladr_repl Y USING(oldCode, newCode) WHERE (T.oldCode, T.newCode) = (X.oldCode, X.newCode) AND Y IS NULL; -- очищаем совпадающие DELETE FROM _kladr_repl kli USING kladr_repl kl WHERE (kli.oldCode, kli.newCode) = (kl.oldCode, kl.newCode); -- вставляем оставшиеся INSERT INTO kladr_repl SELECT * FROM _kladr_repl;
Тут мы использовали еще две "производные" таблицы, которые выступают в качестве поисковых индексов - kladr_kw и kladr_hs:
-- ключевые слова адресных объектов CREATE TABLE kladr_kw( code varchar, status varchar, keyword varchar ); CREATE INDEX "kladr-kw-cd" ON kladr_kw(code, status); CREATE INDEX "kladr-kw-kwcd" ON kladr_kw(keyword varchar_pattern_ops, code varchar_pattern_ops); CREATE INDEX "kladr-kw-cdkw" ON kladr_kw(code varchar_pattern_ops, keyword varchar_pattern_ops); -- группы номеров домов с одинаковыми признаками CREATE TABLE kladr_hs( code varchar, idx varchar, ifns varchar, ocato varchar, houses varchar[] ); CREATE INDEX "kladr-hs-code" ON kladr_hs(code); CREATE INDEX "kladr-hs-idx" ON kladr_hs(idx); -- специальные "уровневые" индексы DO $$ DECLARE ln integer[] = '{2,5,8,11,15}'::integer[]; BEGIN FOR i IN 1..array_length(ln, 1) LOOP EXECUTE ' CREATE INDEX "kladr-' || lpad(ln[i]::text, 2, '0') || '" ON kladr(code varchar_pattern_ops, status) WHERE length(code) = ' || ln[i] || '; CREATE INDEX "kladr-kw-' || lpad(ln[i]::text, 2, '0') || '" ON kladr_kw(keyword varchar_pattern_ops) WHERE length(code) = ' || ln[i] || '; CREATE INDEX "kladr-kw-kwcd-' || lpad(ln[i]::text, 2, '0') || '" ON kladr_kw(keyword varchar_pattern_ops, code varchar_pattern_ops) WHERE length(code) = ' || ln[i] || '; '; END LOOP; END$$ LANGUAGE plpgsql;
Зачем нам понадобятся такие дополнительные структуры, и как их использовать для организации эффективного подстрочного поиска, рассмотрим в следующей части статьи, а эту мы закончим функцией нормализации названия объекта:
normalize.sql
CREATE OR REPLACE FUNCTION normalize(obj kladr) RETURNS text AS $$ SELECT CASE -- регионы WHEN length(code) = 2 THEN regexp_replace( regexp_replace( ( CASE WHEN abbr = 'Респ' AND name !~* '(ая)$' THEN abbrfull || ' ' || name WHEN abbr = 'край' THEN name || ' ' || abbr WHEN abbr = 'обл' THEN name || ' ' || abbr || '.' WHEN abbr = 'г' THEN abbr || '.' || name WHEN abbr IN ('АО', 'Аобл') THEN regexp_replace(name, ' Автономный округ', '', 'ig') || ' АО' ELSE name || ' ' || abbrfull END ), E'/([а-я]+)/', E'- \\1', 'ig' ), E'^(.{0,}?) - ([а-я]+)(.{0,}?)$', E'\\1\\3 (\\2)', 'ig' ) -- районы WHEN length(code) = 5 THEN CASE -- (14xxx) улус WHEN abbr = 'у' THEN initcap(name) || ' ' || lower(abbrfull) -- (24048) WHEN abbr = 'АО' THEN initcap(name) || ' ' || abbr ELSE initcap(name) || ' ' || abbr || (CASE WHEN abbr ~ '(-|/)' THEN '' ELSE '.' END) END -- города и населенные пункты WHEN length(code) IN (8, 11) THEN CASE WHEN abbr IN ('тер', 'г', 'с/п', 'дп') THEN regexp_replace( regexp_replace( CASE WHEN name ~* E'^ДНП ' THEN regexp_replace(name, E'^ДНП ', 'днп ', 'ig') WHEN name ~* E'^ДНТ ' THEN regexp_replace(name, E'^ДНТ ', 'днт ', 'ig') WHEN name ~* E'^ДСК ' THEN regexp_replace(name, E'^ДСК ', 'дск ', 'ig') WHEN name ~* E'^ДСПК ' THEN regexp_replace(name, E'^ДСПК ', 'дспк ', 'ig') WHEN name ~* E'^ДПК ' THEN regexp_replace(name, E'^ДПК ', 'дпк ', 'ig') WHEN name ~* E'^НСТ ' THEN regexp_replace(name, E'^НСТ ', 'нст ', 'ig') WHEN name ~* E'^СДТ ' THEN regexp_replace(name, E'^СДТ ', 'сдт ', 'ig') WHEN name ~* E'^СК ' THEN regexp_replace(name, E'^СК ', 'ск ', 'ig') WHEN name ~* E'^С/К ' THEN regexp_replace(name, E'^С/К ', 'ск ', 'ig') WHEN name ~* E'^СКТ ' THEN regexp_replace(name, E'^СКТ ', 'скт ', 'ig') WHEN name ~* E'^СТ ' THEN regexp_replace(name, E'^СТ ', 'ст ', 'ig') WHEN name ~* E'^С/Т ' THEN regexp_replace(name, E'^С/Т ', 'ст ', 'ig') WHEN name ~* E'^СНТ ' THEN regexp_replace(name, E'^СНТ ', 'снт ', 'ig') WHEN name ~* E'^ПКС ' THEN regexp_replace(name, E'^ПКС ', 'пкс ', 'ig') WHEN name ~* E'^ПКСТ ' THEN regexp_replace(name, E'^ПКСТ ', 'пкст ', 'ig') WHEN name ~* E'^ПОС ' THEN regexp_replace(name, E'^ПОС ', 'пос ', 'ig') WHEN name ~* E'^массив ' THEN regexp_replace(name, E'^массив ', 'массив ', 'ig') WHEN name ~* E' СНТ$' THEN regexp_replace(name, E'^(.*) СНТ$', E'снт \\1', 'ig') WHEN name ~* E' СТ$' THEN regexp_replace(name, E'^(.*) СТ$', E'ст \\1', 'ig') WHEN name ~* E' ДНТ$' THEN regexp_replace(name, E'^(.*) ДНТ$', E'днт \\1', 'ig') WHEN name ~* E' ДПК$' THEN regexp_replace(name, E'^(.*) ДПК$', E'дпк \\1', 'ig') WHEN name ~* E'^уч-к СДТ ' THEN regexp_replace(name, E'^уч-к СДТ ', 'сдт ', 'ig') WHEN name ~* E'^уч-к ' THEN regexp_replace(name, E'^уч-к ', '', 'ig') WHEN name ~* E'^СПК ' THEN regexp_replace(name, E'^СПК ', 'спк ', 'ig') WHEN name ~* E'^сот ' THEN regexp_replace(name, E'^сот ', 'сот ', 'ig') WHEN name ~* E'^урочище ' THEN regexp_replace(name, E'^урочище ', 'урочище ', 'ig') WHEN name ~* E'^КС ' THEN regexp_replace(name, E'^КС ', 'кс ', 'ig') WHEN name ~* E'^МО пгт.' THEN regexp_replace(name, E'^МО пгт.', 'пгт ', 'ig') WHEN name ~* E'^МО город ' THEN regexp_replace(name, E'^МО город ', 'г.', 'ig') WHEN name ~* E'^МО городское поселение ' THEN regexp_replace(name, E'^МО городское поселение ', 'г/п ', 'ig') WHEN name ~* E'^МО поселок ' THEN regexp_replace(name, E'^МО поселок ', 'пос.', 'ig') WHEN name ~* E'^МО СП ' THEN regexp_replace(name, E'^МО СП (.*)', E'с/п \\1', 'ig') WHEN name ~* E'^МО ГП поселок ' THEN regexp_replace(name, E'^МО ГП поселок (.*)', E'пгт \\1', 'ig') WHEN name ~* E'^МО сельское пос��ление ' THEN regexp_replace(name, E'^МО сельское поселение (.*)', E'с/п \\1', 'ig') WHEN name ~* E'^МО селское поселение ' THEN regexp_replace(name, E'^МО селское поселение (.*)', E'с/п \\1', 'ig') WHEN name ~* E'^МО (.*) сельское поселение$' THEN regexp_replace(name, E'^МО (.*) сельское поселение$', E'с/п \\1', 'ig') WHEN name ~* ' сельское поселение$' THEN regexp_replace(name, '^(.*) сельское поселение$', E'с/п \\1', 'ig') WHEN name ~* ' селское поселение$' THEN regexp_replace(name, '^(.*) селское поселение$', E'с/п \\1', 'ig') WHEN name ~* ' городское поселение$' THEN regexp_replace(name, '^(.*) городское поселение$', E'г/п \\1', 'ig') WHEN name ~* ' городской округ$' THEN regexp_replace(name, '^(.*) городской округ$', E'\\1 г/о', 'ig') WHEN name ~* '^Городское поселение ' THEN regexp_replace(name, '^Городское поселение ', E'г/п ', 'ig') ELSE CASE WHEN abbr IN ('с/п', 'дп') THEN abbr || ' ' || name ELSE abbr || '.' || name END END, E'^(г/п|с/п) (.*(?:ое))$', E'\\2 \\1', 'ig' ), E'(\\S)\\(', E'\\1 (', 'ig' ) WHEN abbr IN ('высел') THEN CASE WHEN name ~ E'(.*((?:ие)))$' THEN name || ' выселки' ELSE 'выселок ' || name END WHEN abbr IN ('городок') THEN CASE WHEN name ~* E'(^городок|городок$)' THEN name WHEN name ~* E'военный' THEN regexp_replace(name, E'(военный)(\\s?)', E'\\1 городок\\2', 'ig') ELSE abbr || ' ' || name END WHEN abbr IN ('с/а', 'с/мо') THEN regexp_replace(abbr || ' ' || name, E'^(' || abbr || ') (.*((?:ая)|(?:ое)|(?:ий)|(?:ый)))$', E'\\2 \\1', 'ig') WHEN abbr IN ('п/о', 'пгт', 'массив') THEN abbr || ' ' || name WHEN abbr = 'с/с' THEN ( CASE WHEN name ~* E'^Рабочий поселок' THEN regexp_replace(name, E'^Рабочий поселок', 'рабочий поселок', 'ig') WHEN name ~* E'^р\\.п\\.' THEN regexp_replace(name, E'^р\\.п\\.', 'рабочий поселок', 'ig') ELSE name || ' ' || abbr END ) WHEN abbr = 'ж/д_будка' THEN 'ж/д будка ' || regexp_replace(name, E'(будка|будка железной дороги|железнодорожная будка)\\s*', E'', 'ig') WHEN abbr = 'ж/д_казарм' THEN 'ж/д казарма ' || regexp_replace(name, E'(казарма)\\s*', E'', 'ig') WHEN abbr = 'ж/д_оп' THEN 'ж/д о/п ' || name WHEN abbr = 'ж/д_платф' THEN 'ж/д платформа ' || regexp_replace(name, E'(платформа|платформа железной дороги|железнодорожная платформа)\\s*', E'', 'ig') WHEN abbr = 'ж/д_пост' THEN CASE WHEN name ~* E'^Блокпост' THEN name WHEN name ~* E'^(железнодорожная )?будка' THEN regexp_replace(name, E'^(железнодорожная )?будка', E'ж/д будка', 'ig') WHEN name ~* E'^разъезд ' THEN regexp_replace(name, E'^разъезд', E'ж/д разъезд', 'ig') ELSE 'ж/д пост ' || name END WHEN abbr = 'ж/д_рзд' THEN 'ж/д разъезд ' || name WHEN abbr = 'ж/д_ст' THEN 'ж/д станция ' || name WHEN abbr = 'казарма' THEN CASE WHEN name ~* E'^Блокпост' THEN name WHEN name ~* E'^железнодорожная\\s+' THEN regexp_replace(name, E'^железнодорожная\\s+', E'ж/д казарма ', 'ig') ELSE 'казарма ' || regexp_replace(name, E'^казарма\\s*', E'', 'ig') END WHEN abbr = 'мкр' THEN CASE WHEN name ~* E'^(.*-й|\\d+)$' THEN name || ' ' || lower(abbrfull) WHEN name ~* E'^(\\d+[а-я])$' THEN lower(abbrfull) || ' ' || name ELSE abbr || '.' || name END WHEN abbr IN ('нп','с','д','тер','п','жилзона','жилрайон','кв-л','х','ст-ца','рп','рзд','м','ст','у','снт','сл') THEN CASE WHEN name ~* E'^остров\\s+' THEN regexp_replace(name, E'^остров', E'остров', 'ig') WHEN name ~* E'^казарма\\s+' THEN regexp_replace(name, E'^казарма', E'казарма', 'ig') WHEN name ~* E'^кордон\\s+' THEN regexp_replace(name, E'^кордон', E'кордон', 'ig') WHEN name ~* E'^жилой район ' THEN regexp_replace(name, E'^жилой район ', 'жилой район ', 'ig') WHEN name ~* E' жилой район$' THEN regexp_replace(name, E'^(.*) жилой район$', E'жилой район \\1', 'ig') WHEN name ~* E'^жилая зона ' THEN regexp_replace(name, E'^жилая зона ', 'жилая зона ', 'ig') WHEN name ~* E' жилая зона$' THEN regexp_replace(name, E'^(.*) жилая зона$', E'жилая зона \\1', 'ig') WHEN name ~* E'^д/о ' THEN regexp_replace(name, E'^д/о ', 'д/о ', 'ig') WHEN name ~* E'^п/о ' THEN regexp_replace(name, E'^п/о ', 'п/о ', 'ig') WHEN name ~* E'^п/л ' THEN regexp_replace(name, E'^п/л ', 'п/л ', 'ig') WHEN name ~* E'^т/б ' THEN regexp_replace(name, E'^т/б ', 'т/б ', 'ig') WHEN name ~* E'^СНТ ' THEN regexp_replace(name, E'^СНТ ', 'снт ', 'ig') WHEN name ~* E' СНТ$' THEN regexp_replace(name, E'^(.*) СНТ$', E'снт \\1', 'ig') WHEN name ~* E'^СДТ ' THEN regexp_replace(name, E'^СДТ ', 'сдт ', 'ig') WHEN name ~* E' СДТ$' THEN regexp_replace(name, E'^(.*) СДТ$', E'сдт \\1', 'ig') WHEN name ~* E'^СТ ' THEN regexp_replace(name, E'^СТ ', 'ст ', 'ig') WHEN name ~* E' СТ$' THEN regexp_replace(name, E'^(.*) СТ$', E'ст \\1', 'ig') WHEN name ~* E'^ДНТ ' THEN regexp_replace(name, E'^ДНТ ', 'днт ', 'ig') WHEN name ~* E' ДНТ$' THEN regexp_replace(name, E'^(.*) ДНТ$', E'днт \\1', 'ig') WHEN name ~* E'^ДНП ' THEN regexp_replace(name, E'^ДНП ', 'днп ', 'ig') WHEN name ~* E' ДНП$' THEN regexp_replace(name, E'^(.*) ДНП$', E'днп \\1', 'ig') WHEN name ~* E'^будка( (ж/д|железной дороги))?' THEN regexp_replace(name, E'^будка( (ж/д|железной дороги))?', E'ж/д будка', 'ig') WHEN name ~* E'^железнодорожная будка' THEN regexp_replace(name, E'^железнодорожная будка', E'ж/д будка', 'ig') WHEN name ~* E'^платф.+' THEN regexp_replace(name, E'^(платф)\\s+', E'платформа ', 'ig') WHEN name ~* E'^поселок.+' THEN regexp_replace(name, E'^(поселок)\\s+', E'п.', 'ig') WHEN name ~* E'^(рзд|разъезд).+' THEN regexp_replace(name, E'^(рзд|разъезд)\\s+', E'разъезд ', 'ig') WHEN name ~* E'^участок.+' THEN regexp_replace(name, E'^(участок)\\s+', E'участок ', 'ig') WHEN name ~* E'^(урочище|ур-ще) ' THEN regexp_replace(name, E'^(урочище|ур-ще) ', 'урочище ', 'ig') -- WHEN name ~* E'^ж/д' THEN -- name ELSE CASE WHEN abbr IN ('с', 'д', 'тер', 'п') THEN abbr || '.' || name WHEN abbr IN ('жилзона','жилрайон','кв-л','х','ст-ца','рп','рзд','м','ст','у') THEN lower(abbrfull) || ' ' || name WHEN abbr IN ('снт') THEN abbr || ' ' || name WHEN abbr IN ('сл') THEN regexp_replace(lower(abbrfull) || ' ' || name, E'^(' || lower(abbrfull) || ') (.*((?:ая)))$', E'\\2 \\1', 'ig') WHEN abbr IN ('нп') THEN 'н/п ' || name ELSE name END END ELSE CASE WHEN length(code) = 8 THEN name || ' ' || abbr ELSE abbr || ' ' || name END END -- улицы WHEN length(code) = 15 THEN CASE WHEN abbr IN ('высел') THEN CASE WHEN name ~ E'(.*((?:ие)))$' THEN name || ' выселки' ELSE 'выселок ' || name END WHEN abbr IN ('гск') THEN CASE WHEN name ~* E'^ГСК ' THEN regexp_replace(name, E'^ГСК ', 'гск ', 'ig') WHEN name ~* E'^Г СК ' THEN regexp_replace(name, E'^Г СК ', 'гск ', 'ig') ELSE lower(abbr) || ' ' || name END WHEN abbr IN ('ж/д_будка') THEN CASE WHEN name ~* E'^((железнодорожная )?будка|ждб|ж-д будка)' THEN regexp_replace(name, E'^((железнодорожная )?будка|ждб|ж-д будка)', E'ж/д будка', 'ig') WHEN name ~* E'^железнодорожная\\s+' THEN regexp_replace(name, E'^железнодорожная', E'ж/д будка', 'ig') WHEN name ~* E'^железнодорожная$' THEN regexp_replace(name, E'^железнодорожная$', E'Железнодорожная будка', 'ig') ELSE 'ж/д будка ' || name END WHEN abbr IN ('ж/д_казарм') THEN CASE WHEN name ~* E'^казарма\\s+' THEN regexp_replace(name, E'^казарма', E'ж/д казарма', 'ig') ELSE 'ж/д казарма ' || name END WHEN abbr IN ('ж/д_оп') THEN 'ж/д о/п ' || name WHEN abbr = 'ж/д_платф' THEN 'ж/д платформа ' || regexp_replace(name, E'(платформа|платформа железной дороги|железнодорожная платформа)\\s*', E'', 'ig') WHEN abbr IN ('аал','аул') THEN lower(abbrfull) || ' ' || name WHEN abbr = 'ж/д_пост' THEN CASE WHEN name ~* E'^Блокпост' THEN name WHEN name ~* E'^(железнодорожная )?будка' THEN regexp_replace(name, E'^(железнодорожная )?будка', E'ж/д будка', 'ig') WHEN name ~* E'^разъезд ' THEN regexp_replace(name, E'^разъезд', E'ж/д разъезд', 'ig') ELSE 'ж/д пост ' || name END WHEN abbr = 'ж/д_рзд' THEN 'ж/д разъезд ' || name WHEN abbr = 'ж/д_ст' THEN 'ж/д станция ' || name WHEN abbr = 'жт' THEN CASE WHEN name ~* '^животноводческая' THEN name WHEN name ~* E'^точка\\s' THEN lower(abbrfull) || ' ' || regexp_replace(name, E'^точка ', E'', 'ig') WHEN name ~* E'^ферма ' THEN regexp_replace(name, E'^ферма ', 'ферма ', 'ig') WHEN name ~* E' ферма$' THEN regexp_replace(name, E'^(.*) ферма$', E'ферма \\1', 'ig') ELSE regexp_replace(lower(abbrfull) || ' ' || name, E'^(' || lower(abbrfull) || ') (.*((?:ая)|(?:-я)))$', E'\\2 \\1', 'ig') END WHEN abbr = 'зона' THEN regexp_replace( CASE WHEN name ~* E'^зона ' THEN regexp_replace(name, E'^зона ', 'зона ', 'ig') WHEN name ~* E' зона$' THEN name WHEN name ~* E'^промзона$' THEN name WHEN name ~* E' промзона$' THEN name ELSE regexp_replace(lower(abbrfull) || ' ' || name, E'^(' || lower(abbrfull) || ') (.*((?:ая)|(?:-я)))$', E'\\2 \\1', 'ig') END, E' промышленная зона$', E' промзона', 'ig' ) WHEN abbr = 'казарма' THEN CASE WHEN name ~* E'^Блокпост' THEN name WHEN name ~* E'^железнодорожная\\s+' THEN regexp_replace(name, E'^железнодорожная\\s+', E'ж/д казарма ', 'ig') ELSE 'казарма ' || regexp_replace(name, E'^казарма\\s*', E'', 'ig') END WHEN abbr = 'км' THEN CASE WHEN name ~* E'(\\d|-й)$' THEN name || ' ' || abbr WHEN name ~* E'^([0-9\\.]+)( )?' THEN regexp_replace(name, E'^([0-9\\.]+)( )?(км( |$))?', E'\\1 км ', 'ig') ELSE name END WHEN abbr = 'мкр' THEN CASE WHEN name ~* E'^(.*-й|\\d+)$' THEN name || ' ' || lower(abbrfull) WHEN name ~* E'^(\\d+[а-я])$' THEN lower(abbrfull) || ' ' || name ELSE abbr || '.' || name END WHEN abbr = 'остров' THEN CASE WHEN name ~* E'(.*-й$|\\d+)$' THEN name || ' ' || lower(abbrfull) ELSE lower(abbrfull) || ' ' || name END WHEN abbr IN ('лпх','п/о','п/р','п/ст','промзона') THEN regexp_replace(lower(abbr) || ' ' || name, E'^(' || lower(abbr) || ') (.*((?:ая)|(?:ое)|(?:ой)|(?:ий)|(?:ый)|(?:-й)|(?:-я)))$', E'\\2 \\1', 'ig') WHEN abbr IN ('сад','б-р') THEN CASE WHEN name ~* E'^(.*-й|.*-я|\\d+)$' THEN name || ' ' || lower(abbrfull) ELSE regexp_replace(lower(abbr) || ' ' || name, E'^(' || lower(abbr) || ') (.*((?:ой)|(?:ий)|(?:ый)|(?:-й)))$', E'\\2 \\1', 'ig') END WHEN abbr IN ('пер','пл','туп') THEN CASE WHEN name ~* E'^(.*-й|.*-я|\\d+)$' THEN name || ' ' || lower(abbrfull) ELSE regexp_replace(lower(abbr) || '.' || name, E'^(' || lower(abbr) || E'\\.)(.*((?:ая)|(?:ое)|(?:ой)|(?:ий)|(?:ый)|(?:-й)|(?:-я)))$', E'\\2 \\1', 'ig') END WHEN abbr IN ('х','ш') THEN CASE WHEN name ~* E'^(.*-й|.*-я|\\d+)$' THEN name || ' ' || lower(abbrfull) ELSE regexp_replace(lower(abbr) || '.' || regexp_replace(name, E' ' || lower(abbrfull) || '$', E'', 'ig'), E'^(' || lower(abbr) || E'\\.)(.*((?:ая)|(?:ое)|(?:ой)|(?:ий)|(?:ый)|(?:-й)|(?:-я)))$', E'\\2 ' || lower(abbrfull), 'ig') END WHEN abbr IN ('пр-кт') THEN CASE WHEN name ~* E'^(.*-й|.*-я|\\d+)$' THEN name || ' ' || lower(abbrfull) ELSE regexp_replace(lower('пр-т') || ' ' || name, E'^(' || lower('пр-т') || E')\\s(.*((?:ой)|(?:ий)|(?:ый)|(?:-й)|(?:-я)))$', E'\\2 \\1', 'ig') END WHEN abbr IN ('проезд') THEN CASE WHEN name ~* E'^(.*-й|.*-я|\\d+)$' THEN name || ' ' || lower(abbrfull) ELSE regexp_replace(lower('пр-д') || ' ' || name, E'^(' || lower('пр-д') || E')\\s(.*((?:ой)|(?:ий)|(?:ый)|(?:-й)|(?:-я)))$', E'\\2 \\1', 'ig') END WHEN abbr IN ('стр') THEN CASE WHEN name ~* E'^\\d+-е$' THEN name || ' ' || lower(abbrfull) WHEN name ~* E'^\\d+(-[а-я])?$' THEN lower(abbrfull) || ' ' || name ELSE name END WHEN abbr IN ('аллея','берег','бугор','вал','въезд','городок','дор','заезд','канал','кв-л','кольцо','коса','линия','мост','парк','переезд','платф','пл-ка','полустанок','просек','просека','проселок','проток','проулок','рзд','ряды','сквер','спуск','тракт','уч-к','ферма') THEN CASE WHEN abbr = 'дор' AND name ~* '^(а/д|автодорога)' THEN regexp_replace(name, E'^(а/д|автодорога)\\s', E'а/д ', 'ig') WHEN name ~* E'^(.*-й|.*-я|\\d+)$' THEN name || ' ' || lower(abbrfull) ELSE regexp_replace(lower(abbrfull) || ' ' || name, E'^(' || lower(abbrfull) || ') (.*((?:ая)|(?:ое)|(?:ой)|(?:ий)|(?:ый)|(?:ые)|(?:-й)|(?:-я)))$', E'\\2 \\1', 'ig') END WHEN abbr IN ('ул') THEN replace(abbr, '_', ' ') || (CASE WHEN abbr ~ '(-|/)' THEN '' ELSE '.' END) || '' || name WHEN abbr IN ('нп','с','д','тер','п','жилзона','жилрайон','х','ст-ца','рп','рзд','м','ст','у','снт','сл') THEN CASE WHEN name ~* E'^остров\\s+' THEN regexp_replace(name, E'^остров', E'остров', 'ig') WHEN name ~* E'^казарма\\s+' THEN regexp_replace(name, E'^казарма', E'казарма', 'ig') WHEN name ~* E'^кордон\\s+' THEN regexp_replace(name, E'^кордон', E'кордон', 'ig') WHEN name ~* E'^жилой район ' THEN regexp_replace(name, E'^жилой район ', 'жилой район ', 'ig') WHEN name ~* E' жилой район$' THEN regexp_replace(name, E'^(.*) жилой район$', E'жилой район \\1', 'ig') WHEN name ~* E'^жилая зона ' THEN regexp_replace(name, E'^жилая зона ', 'жилая зона ', 'ig') WHEN name ~* E' жилая зона$' THEN regexp_replace(name, E'^(.*) жилая зона$', E'жилая зона \\1', 'ig') WHEN name ~* E'^д/о ' THEN regexp_replace(name, E'^д/о ', 'д/о ', 'ig') WHEN name ~* E'^п/о ' THEN regexp_replace(name, E'^п/о ', 'п/о ', 'ig') WHEN name ~* E'^п/л ' THEN regexp_replace(name, E'^п/л ', 'п/л ', 'ig') WHEN name ~* E'^т/б ' THEN regexp_replace(name, E'^т/б ', 'т/б ', 'ig') WHEN name ~* E'^СНТ ' THEN regexp_replace(name, E'^СНТ ', 'снт ', 'ig') WHEN name ~* E' СНТ$' THEN regexp_replace(name, E'^(.*) СНТ$', E'снт \\1', 'ig') WHEN name ~* E'^СДТ ' THEN regexp_replace(name, E'^СДТ ', 'сдт ', 'ig') WHEN name ~* E' СДТ$' THEN regexp_replace(name, E'^(.*) СДТ$', E'сдт \\1', 'ig') WHEN name ~* E'^СТ ' THEN regexp_replace(name, E'^СТ ', 'ст ', 'ig') WHEN name ~* E' СТ$' THEN regexp_replace(name, E'^(.*) СТ$', E'ст \\1', 'ig') WHEN name ~* E'^ДНТ ' THEN regexp_replace(name, E'^ДНТ ', 'днт ', 'ig') WHEN name ~* E' ДНТ$' THEN regexp_replace(name, E'^(.*) ДНТ$', E'днт \\1', 'ig') WHEN name ~* E'^ДНП ' THEN regexp_replace(name, E'^ДНП ', 'днп ', 'ig') WHEN name ~* E' ДНП$' THEN regexp_replace(name, E'^(.*) ДНП$', E'днп \\1', 'ig') WHEN name ~* E'^будка( (ж/д|железной дороги))?' THEN regexp_replace(name, E'^будка( (ж/д|железной дороги))?', E'ж/д будка', 'ig') WHEN name ~* E'^железнодорожная будка' THEN regexp_replace(name, E'^железнодорожная будка', E'ж/д будка', 'ig') WHEN name ~* E'^платф.+' THEN regexp_replace(name, E'^(платф)\\s+', E'платформа ', 'ig') WHEN name ~* E'^поселок.+' THEN regexp_replace(name, E'^(поселок)\\s+', E'п.', 'ig') WHEN name ~* E'^(рзд|разъезд).+' THEN regexp_replace(name, E'^(рзд|разъезд)\\s+', E'разъезд ', 'ig') WHEN name ~* E'^участок.+' THEN regexp_replace(name, E'^(участок)\\s+', E'участок ', 'ig') WHEN name ~* E'^(урочище|ур-ще) ' THEN regexp_replace(name, E'^(урочище|ур-ще) ', 'урочище ', 'ig') -- WHEN name ~* E'^ж/д' THEN -- name ELSE CASE WHEN abbr IN ('с', 'д', 'тер', 'п') THEN abbr || '.' || name WHEN abbr IN ('жилзона','жилрайон','кв-л','х','ст-ца','рп','рзд','м','ст','у') THEN lower(abbrfull) || ' ' || name WHEN abbr IN ('снт') THEN abbr || ' ' || name WHEN abbr IN ('сл') THEN regexp_replace(lower(abbrfull) || ' ' || name, E'^(' || lower(abbrfull) || ') (.*((?:ая)))$', E'\\2 \\1', 'ig') WHEN abbr IN ('нп') THEN 'н/п ' || name ELSE name END END ELSE CASE WHEN name ~* '((?:ая)|(?:ое)|(?:ой)|(?:ий)|(?:ый)|(?:-й)|(?:-я))$' THEN name || ' ' || replace(abbr, '_', ' ') || (CASE WHEN abbr ~ '(-|/)' THEN '' ELSE '.' END) ELSE replace(abbr, '_', ' ') || (CASE WHEN abbr ~ '(-|/)' THEN '' ELSE '.' END) || name END END END FROM ( SELECT kl.*, ab.name abbrfull FROM ( SELECT ($1).* ) kl LEFT JOIN kladr_abbr ab ON ab.code = kl.abbr AND ( (ab.lvl / 100, length(kl.code)) = (5, 15) OR (ab.lvl / 100, length(kl.code)) = (4, 11) OR (ab.lvl / 100, length(kl.code)) = (3, 8) OR (ab.lvl / 100, length(kl.code)) = (2, 5) OR (ab.lvl / 100, length(kl.code)) = (1, 2) ) ) T $$ LANGUAGE sql; CREATE OR REPLACE FUNCTION kladr$norm() RETURNS trigger AS $$ BEGIN -- проверяем необходимость логгирования при обновлении записи NEW.norm = normalize(NEW); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER norm BEFORE INSERT OR UPDATE OF code, name, abbr ON kladr FOR EACH ROW EXECUTE PROCEDURE kladr$norm();
