КЛАДРируем адреса произвольной формы (ч.1 — импорт)

от автора

Достаточно часто при работе с вводимыми пользователем адресными данными возникает необходимость сначала подсказать ему, как правильно свой адрес указать, а потом — полученную строку адреса привести в некоторый машинно-читаемый вид.

Таким видом у нас в стране можно считать код по справочникам КЛАДР или ФИАС.

Первый из них уже несколько лет считается устаревающим, но отличается более простой структурой и исторически продолжает использоваться во множестве систем, поскольку вполне подходит для большинства задач.

Давайте научимся разбирать строку адреса «туда и обратно», а заодно познакомимся с некоторыми алгоритмическими подходами и их реализацией на 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();


ссылка на оригинал статьи https://habr.com/ru/articles/565850/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *