В данной статье я продолжу исследование данных OpenStreetMap (начало смотри тут). В этот раз я рассмотрю работу с данными административного деления. То есть, те самые области, края, регионы и т д.

Работать буду с данными по России, если у вас не установлен SSD и слабый процессор, и вы просто хотите потренироваться, то я рекомендую взять данные какого-нибудь отдельного округа: download.geofabrik.de/russia.html, тогда все действия будут выполняться куда быстрее.

Конвертировать данные я буду с помощью утилиты imposm3, как ее установить я подробнее рассмотрел в предыдущей статье, так что в этой я только опишу процесс создания файла для мэппинга данных. Создадим файл mapping.yaml и заполним его следующим образом:

tables:
  boundaries:
    type: "polygon" # тип геометрии используемый для таблицы, по сути, говорим что будем брать данные из таблички multipolygons
    mapping: # фильтрация по наличия поля, и значению в этом поле
      boundary: [__any__] # любое не NULL значение, какие значения бывают https://wiki.openstreetmap.org/wiki/RU:Key:boundary
    columns: # перечисляем поля таблицы
      - {name: osm_id, type: id}                                   # идентификатор OpenStreetMap
      - {name: geometry, type: geometry}                           # геометрия
      - {name: type, type: mapping_value}                          # тип границы, одно из возможных значений boundry
      - {key: name, name: name, type: string}                      # поле с именем объекта
      - {key: admin_level, name: admin_level, type: integer}       # уровень административной границы
      - {key: "wikidata", name: wikidata, type: string}            # идентификатор из wikidata
      - {key: "wikipedia", name: wikipedia, type: string}          # название из википедии
      - {key: "hstore_tags", name: hstore_tags, type: hstore_tags} # оставшиеся тэги в это поле
tags:
  load_all: true # чтобы в поле hstore_tags, сохранялись все тэги

Создадим базу данных и подключим расширения:

createdb geoadmin
echo 'CREATE EXTENSION postgis; CREATE EXTENSION hstore;' | psql -d geoadmin

Создадим файл конфигурации imposm3_config.json:

{
  "cachedir": "./imposm3_cache",
  "connection": "postgis://user:password@localhost/geoadmin",
  "mapping": "mapping.yaml",
}

И запустим конверсию:

imposm3 import -config imposm3_config.json -read data.osm.pbf -write -overwritecache -dbschema-import public

Давайте взглянем, что у нас лежит в БД. У нас там одна таблица osm_boundaries. В ней лежат различные области, края, районы и т.д.
Выведем все “области”, которые есть в таблице:

SELECT name, admin_level, st_npoints(geometry) 
FROM osm_boundaries 
WHERE name ~ 'область' 
ORDER BY name

Получим что-то в этом роде:

Наименование Административный уровень Количество точек в геометрии
Амурская область 4 19837
Архангельская область 4 4197
Астраханская область 4 2310
Белгородская область 4 4015

Все области, края и прочие крупные административные объекты имеют уровень 4.
А районы, на которые, как правило, разбиты всякие “области”, имеют уровень 6.

Я не случайно вывел количество точек в геометрии. Так как я планирую выводить в реальном времени все области определенного уровня сразу,
то количество точек у каждого объекта, если по отдельности и не велико, то в сумме окажется весьма большим, что отрицательно скажется на производительности.
Поэтому нам придется подумать над тем, чтобы упростить геометрию объектов. Но это мы рассмотрим чуть позже.

Давайте временно отвлечемся от базы данных и напишем визуализатор. Я воспользуюсь своей любимой комбинацией Flask + Vue.

Пишем бэкенд на Flask

Установим необходимые пакеты:

pip install Flask flask-peewee psycopg2

Создадим файл app.py со следующим содержимым:

from flask import Flask, jsonify
from flask_peewee.db import Database

# настройки подключения
DATABASE = {
    'name': 'geoadmin',  # укажите свою БД
    'engine': 'peewee.PostgresqlDatabase',
    'user': 'user',  # тут юзера своего
    'password': 'password',  # а тут пароль для подключения к БД
    'host': 'localhost',
}
DEBUG = True
SECRET_KEY = 'ssshhhh'

# создаем приложение, и подключаем к нему peewee БД
app = Flask(__name__)
app.config.from_object(__name__)
db = Database(app)


# возвращает границу
@app.route("/api/border/")
def border():
    # наш запрос
    q = db.Model.raw("""
SELECT
  st_asgeojson(geometry) AS geometry
FROM osm_boundaries
WHERE name ~ 'Иркутская'  -- тут выбрать любой регион,
                          -- не сильно большой, не сильно маленький
""")

    # если регионов несколько, то собираем их в список
    geometries = []
    for row in q:
        geometries.append(json.loads(row.geometry))

    # возвращаем запрос
    return jsonify({
        'geometries': geometries
    })


if __name__ == '__main__':
    app.run()

Пишем фронтенд на Vue.js

Подготовка

Устанавливаем vue-cli:

npm install -g vue-cli

Создаем папку client и в ней vue приложение:

cd client
vue-init webpack-simple .

Добавим пакеты, которые будем использовать:

yarn add axios leaflet

И установим их:

yarn install

Структура проекта будет выглядеть примерно так:

Alt Text

А теперь отредактируем webpack.config.js. Чтобы корректно подключились leaflet шрифты, заменим:

...
{
    test: /\.(png|jpg|gif|svg)$/,
    loader: 'file-loader',
    options: {
      name: '[name].[ext]?[hash]'
    }
}
...

на

...
{
    test: /\.(png|jpg|gif|svg|ttf|woff|woff2)$/,
    loader: 'file-loader',
    options: {
      name: '[name].[ext]?[hash]'
    }
}
...

Настроим прокси с фронт-сервера на flask-бэкенд. Для этого найдите узел devServer и приведите его к следующему виду:

...
devServer: {
  historyApiFallback: true,
  noInfo: true,
  overlay: true,
  proxy: {
    '/api': 'http://localhost:5000'
  }
},
...

Создаем приложение

Приведем файл client/src/App.vue к следующему виду:

<template>
  <div>
    <div ref="map" id="map"></div>
    <button id="reload-button" @click="updateMapData">Reload</button>
  </div>
</template>


<style lang="scss">
  // стиля для leaflet
  @import "~leaflet/dist/leaflet.css";

  #reload-button {
    position: absolute;
    right: 16px;
    top: 16px;
    z-index: 1000;
  }

  #map {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    top: 0;
  }
</style>


<script>
  import L from 'leaflet';
  import axios from 'axios';

  export default {
    name: 'app',
    data() {
      return {
        map: null, // инстанс leaflet карты
        geometryGeoJSON: null, // инстанс geoJSON объекта
      }
    },
    mounted() {
      // когда данный Vue компонент подключилось к dom дереву
      // создаем leaflet карту,
      // this.$refs.map -- это ссылка на на <div ref="map" id="map">
      this.map = L.map(this.$refs.map).setView([58.505, 108.0], 5);

      // подключаем карту тайлов на базе OpenStreetMap к инстансу карты
      L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
        attribution: '&copy;<a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
      }).addTo(this.map);

      // добавим область, но пока ее не будет видно на карте
      // так как к ней не привязаны данные
      this.geometryGeoJSON = L.geoJSON();
      this.geometryGeoJSON.addTo(this.map);
    },
    methods: {
      updateMapData() {
        // метод загружающий область и отображающий ее на карте
        axios(`/api/border/`).then(r => {
          // чистим геометрию от старых данных
          this.geometryGeoJSON.clearLayers();

          // подцепляем новую геометрию, которую вернул сервер
          this.geometryGeoJSON.addData(r.data.geometries);
        });
      },
    }
  }
</script>

Запустим параллельно бэкенд и фронтенд серверы:

python app.py
yarn dev

Откроем теперь браузер по адресу http://localhost:8080/ и увидим:

Alt Text

Давайте теперь нажмем на кнопку Reload в правом верхнем углу, которая инициализирует нам загрузку геометрии с сервера и отобразит ее на карте:

Alt Text

Возможно, вы заметили небольшое подтормаживание, для того, чтобы удостовериться в своей наблюдательности, переключимся в консоль браузера (F12)
и глянем сколько данных пришло к нам с сервера. Ни много, ни мало, а почти 4.5MB данных:

Alt Text

Вряд ли пользователи скажут нам спасибо за столь бездарно потраченный трафик, более того, leaflet тоже не скажет нам спасибо, рисовать такое количество данных на карте - занятие не из легких, которое весьма отрицательно скажется на производительности.

Тут определенно надо что-то менять. Вот бы как-нибудь сократить количество данных…

Оптимизируем геометрию

К счастью для нас, PostGis предоставляет два метода для упрощения геометрии:

  • st_simplify
  • st_simplifypreservetopology

Оба метода принимают два параметра: геометрию и необходимую точность приближения при выполнения сглаживания.
Используется алгоритм Рамера — Дугласа — Пекера.

Как можно догадаться по названию, st_simplifypreservetopology действует более аккуратно, и сохраняет топологию геометрии.

Попробуйте запустить следующий запрос в БД:

SELECT
  st_npoints(geometry), -- количество точек до оптимизации
  st_npoints(st_simplifypreservetopology(geometry, 1000)) -- количество точек после оптимизации
FROM osm_boundaries
WHERE name ~ 'Иркутская'

Как видно, оптимизация весьма хороша, работает мгновенно и выдает всего 2897 точки против 53715 точек исходной геометрии.
Насколько же хорошее получает отображение?

Давайте переключимся на backend и отредактируем файл app.py:

    ...
    q = db.Model.raw("""
SELECT
  st_asgeojson(
      st_transform(
        st_simplifypreservetopology(
          geometry,
          1000
        ),
        4326
      )
  )::json AS geometry
FROM osm_boundaries
WHERE name ~ 'Иркутская'
""")
    ...

Нажав кнопку Reload на клиенте, получим:

Alt Text

Без линейки разницы не видно. Заглянем в консоль:

Alt Text

Вот теперь другое дело, оптимизации в 20 раз! Неплохо, по-моему. Можно поэкспериментировать со значением:

Alt Text

Как можно догадаться, в зависимости от зума может потребоваться та или иная степень сглаживания.
Сильная степень упрощения геометрии при близком приближении выглядит некрасиво. Вот, например (пунктирная линия это контур исходной геометрии):

Alt Text

Давайте исправим эту проблему.

Синхронизируем степень сглаживания с уровнем зума

Переключимся на клиент, отредактируем скрипт в файле App.vue. Добавим уровень зума карты в качестве параметра get запроса /api/border/ и обработчик события zoomend, больше ничего трогать не будем:

mounted() {
  this.map = L.map(this.$refs.map).setView([58.505, 108.0], 5);

  // добавим реакцию на зум карты
  // при зуме карты, будут запрашиваться данные в новом масштабе
  this.map.on("zoomend", e => {
    this.updateMapData()
  });
  ...
},
methods: {
  updateMapData() {
    axios(`/api/border/`, {
      params: {
        zoom: this.map.getZoom() // добавим уровень зума карты в качестве параметров get запроса
      }
    }).then(r => {...});
  },
}

Теперь откроем app.py и отредактируем его следующим образом:

...
@app.route("/api/border/")
def border():
    from flask import request

    # считываем значение зума из параметра
    zoom = int(request.args['zoom'])

    # подобранные на глаз, значения сглаживания
    tolerance = {
        1: 60000,
        2: 50000,
        3: 25000,
        4: 12000,
        5: 6000,
        6: 4000,
        7: 1500,
        8: 1000,
        9: 350,
        10: 180,
        11: 80,
        12: 50,
        13: 40,
    }.get(zoom, 0)

    # наш запрос
    q = db.Model.raw("""
SELECT
  st_asgeojson(
      st_transform(
        st_simplifypreservetopology(
          geometry,
          %s -- сюда будет подставляться значение tolerance
        ),
        4326
      )
  )::json AS geometry
FROM osm_boundaries
WHERE name ~ 'Иркутская'
""", tolerance)  # тут добавился параметр tolerance

    ... # остальное без изменений

Тестируем:

Alt Text

Выглядит прекрасно, но все равно как-то подтормаживает, особенно на сильном приближении. А это потому, что раз leaflet показывает нам кусок геометрии, это совсем не значит, что с сервера нам присылают этот самый кусок - нам присылают все, что есть. И, если приближение достаточно велико и уровень сглаживания равен 0, то и присылают нам те самые 4.5MB, от которых мы, казалось, так легко избавились до этого.

Вот так каждый новый зум увеличивает нам количество данных с сервера:

Alt Text

Непорядок, надо исправлять.

Отсекаем лишнюю геометрию

И как быть? Очевидно, надо запрашивать часть данных.

Со стороны фронта

Для реализации такого функционала у leaflet карты есть замечательный метод getBounds, который возвращает информацию об области, отображаемой на экране. Давайте отредактируем файл App.vue.

Подцепим еще одно событие moveend к карте с тем же обработчиком. Замените в методе mounted:

mounted() {
  this.map = L.map(this.$refs.map).setView([58.505, 108.0], 5);

  this.map.on("zoomend", e => {
    this.updateMapData()
  });
  ...
}

на

mounted() {
  this.map = L.map(this.$refs.map).setView([58.505, 108.0], 5);

  this.map.on({
    "zoomend": this.updateMapData,
    "moveend": this.updateMapData
  });
  ...
}

И добавим информацию о границах видимой карты в запрос:

methods: {
  updateMapData() {
    // метод загружающий область и отображающий ее на карте
    axios(`/api/border/`, {
      params: {
        zoom: this.map.getZoom(),
        bounds: this.map.getBounds().toBBoxString() // вот новый параметр
      }
    }).then(r => {...})
  }
}

Метод toBBoxString вернет информацию о границах в виде строки из четырех float значений. Т.е., если this.map.getBounds() возвращает:

{
  "_southWest": {
    "lat": 49.03786794532644,
    "lng": 85.42968750000001
  },
  "_northEast": {
    "lat": 65.44000165965537,
    "lng": 123.17871093750001
  }
}

то toBBoxString вернет:

85.42968750000001,49.03786794532644,123.17871093750001,65.44000165965537

Со стороны бэка

Для того, чтобы убрать лишние данные из геометрии, к нам на помощь приходит PostGIS, в этот раз он предлагает метод ST_Intersection.

Идея такая: мы берем геометрию области, формируем геометрию видимой области карты и рассчитываем область, получающуюся, как область пересечения границы с геометрией.

Тут два момента:

1. Надо сформировать геометрию, пригодную для PostGIS из точек границы. Для этого нам пригодится метод ST_GeomFromText.

2. Необходимо, чтобы геометрия границы имела ту же проекцию, что геометрия области.

Отредактируем обработчик border в app.py:

@app.route("/api/border/")
def border():
    from flask import request
    # считываем значение зума из параметра
    zoom = int(request.args['zoom'])

    # формируем полигон
    bounds = [float(i) for i in request.args['bounds'].split(',')]
    bounds_polygon = "POLYGON((" \
                     "{west} {north}," \
                     "{east} {north}," \
                     "{east} {south}," \
                     "{west} {south}," \
                     "{west} {north}" \
                     "))".format(
        west=max(-180, bounds[0]),
        south=bounds[1],
        east=min(180, bounds[2]),
        north=bounds[3],
    )

    # подобранные на глаз, значения сглаживания
    tolerance = {
        1: 60000,
        2: 50000,
        3: 25000,
        4: 12000,
        5: 6000,
        6: 4000,
        7: 1500,
        8: 1000,
        9: 350,
        10: 180,
        11: 80,
        12: 50,
        13: 40,
    }.get(zoom, 0)

    # наш запрос
    q = db.Model.raw("""
SELECT
  st_asgeojson(
      st_transform(
        st_simplifypreservetopology(
          st_intersection( -- рассчитываем пересечение
            geometry,
            st_transform(
              st_geomfromtext(
                %s,
                4326 -- getBounds возвращает геометрию границы в WGS84
              ),
              3857 -- конвертируем в меркатора, чтоб проекции совпадали
            ) -- геометрия границы
          ),
          %s
        ),
        4326
      )
  )::json AS geometry
FROM osm_boundaries
WHERE name ~ 'Иркутская'
""", bounds_polygon, tolerance)

    # если регионов несколько, то собираем их в список
    geometries = []
    for row in q:
        geometries.append(row.geometry)

    # возвращаем запрос
    return jsonify({
        'geometries': geometries
    })

Давайте теперь протестируем:

Alt Text

Проверим в консоли (там красота):

Alt Text

Подцепляем данные к геометрии

Очень может наверняка, но вам захочется какие-то данные к геометрии подцепить, да так, чтобы leaflet все это понял и геометрию построил и данные показал.

Для этого надо сначала посмотреть как выглядит наша область в виде geojson. А выглядит она примерно так:

[
    {
      "coordinates": [
        [
          [
            109.871487390779,
            56.8639416730006
          ],
          ...
        ]
      ],
      "type": "Polygon"
    },
    ...
]

Как видно, данные сюда добавить некуда. Поэтому, для того, чтобы привязать данные к области, надо сформировать json объект следующей структуры:

[
    {
      "type": "Feature",  // тип, обязательно такой
      "properties": {  // словарик со свойствами геометрии
         "name": "Иркутская область"
      },
      "geometry": {  // геометрия
        "coordinates": [
          [
            [
              109.871487390779,
              56.8639416730006
            ],
            ...
          ]
        ],
        "type": "Polygon"
      }
    }
]

Для этого вопользуемся Postgresql методом json_build_object и отредактируем запрос в app.py:

    # наш запрос
    q = db.Model.raw("""
WITH regions AS (
    SELECT
        st_simplifypreservetopology(
          st_intersection(geometry, st_transform(st_geomfromtext(%s, 4326), 3857)),
          %s
        ) AS geometry, -- тут не преобразованную геометрию возвращаем
        name as region_name -- возвращаем название региона
    FROM osm_boundaries
    WHERE name ~ 'Иркутская'
)
SELECT json_build_object( -- формируем json объект, заданной структуры
          'type',   'Feature',
          'geometry', st_asgeojson(st_transform(geometry, 4326))::json,
          'properties', json_build_object(
            'name', region_name
          )
       ) as geometry
FROM regions
""", bounds_polygon, tolerance)

Остальное не трогаем.

Теперь давайте поправим App.vue, чтобы при нажатии на область, отображался попап с именем. Заменим:

mounted() {
  ...
  this.geometryGeoJSON = L.geoJSON();
  this.geometryGeoJSON.addTo(this.map);
},

на

mounted() {
  ...
  this.geometryGeoJSON = L.geoJSON(null, {
    onEachFeature: (feature, layer) => {
        // событие по нажатию на область
        layer.bindPopup(feature.properties.name);
    }
  });
  this.geometryGeoJSON.addTo(this.map);
},

А теперь попробуем нажать на область:

Alt Text

Здорово, да?

Выводим больше объектов

До этого мы с одной областью работали, а что это за карта такая? Давайте выводить больше объектов. Но выводить то, что находится за гранью видимой области карты не хочется, поэтому будем проверять с помощью метода st_intersects, лежит ли регион в bounding-box нашей карты. Поправим app.py:

    # наш запрос
    q = db.Model.raw("""
WITH regions AS (
    SELECT
        st_simplifypreservetopology(
          st_intersection(geometry, st_transform(st_geomfromtext(%s, 4326), 3857)),
          %s
        ) AS geometry,
        name as region_name
    FROM osm_boundaries
    WHERE admin_level = 4 and st_intersects(geometry, st_transform(st_geomfromtext(%s, 4326), 3857))
)
SELECT json_build_object(
          'type',   'Feature',
          'geometry', st_asgeojson(st_transform(geometry, 4326))::json,
          'properties', json_build_object(
            'name', region_name
          )
       ) as geometry
FROM regions
""", bounds_polygon, tolerance, bounds_polygon)

Alt Text

Что делать дальше

В принципе, концептуально я рассказал все, что хотел. Что же делать дальше?
Первое, что надо делать - это избавиться от пересчета геометрии методом st_simplifypreservetopology в реальном времени. Из-за него все тормозит. Самый простой вариант - это под каждое значение tolerance добавить отдельно поле и заполнять его рассчитанным значением.

Вот как я это сделал. Я добавил поля к таблице:

ALTER TABLE osm_boundaries ADD COLUMN geometry_60000 geometry;
ALTER TABLE osm_boundaries ADD COLUMN geometry_50000 geometry;
ALTER TABLE osm_boundaries ADD COLUMN geometry_25000 geometry;
ALTER TABLE osm_boundaries ADD COLUMN geometry_12000 geometry;
ALTER TABLE osm_boundaries ADD COLUMN geometry_6000 geometry;
ALTER TABLE osm_boundaries ADD COLUMN geometry_4000 geometry;
ALTER TABLE osm_boundaries ADD COLUMN geometry_1500 geometry;
ALTER TABLE osm_boundaries ADD COLUMN geometry_1000 geometry;
ALTER TABLE osm_boundaries ADD COLUMN geometry_350 geometry;
ALTER TABLE osm_boundaries ADD COLUMN geometry_180 geometry;
ALTER TABLE osm_boundaries ADD COLUMN geometry_80 geometry;
ALTER TABLE osm_boundaries ADD COLUMN geometry_50 geometry;
ALTER TABLE osm_boundaries ADD COLUMN geometry_40 geometry;

Заполнил поля значением (долгий скрипт, на моем четырехъядерном ноутбуке с SSD считал 9 мин):

UPDATE osm_boundaries
SET
  geometry_60000 = st_makevalid(st_simplifypreservetopology(geometry, 60000)),
  geometry_50000 = st_makevalid(st_simplifypreservetopology(geometry, 50000)),
  geometry_25000 = st_makevalid(st_simplifypreservetopology(geometry, 25000)),
  geometry_12000 = st_makevalid(st_simplifypreservetopology(geometry, 12000)),
  geometry_6000 = st_makevalid(st_simplifypreservetopology(geometry, 6000)),
  geometry_4000 = st_makevalid(st_simplifypreservetopology(geometry, 4000)),
  geometry_1500 = st_makevalid(st_simplifypreservetopology(geometry, 1500)),
  geometry_1000 = st_makevalid(st_simplifypreservetopology(geometry, 1000)),
  geometry_350 = st_makevalid(st_simplifypreservetopology(geometry, 350)),
  geometry_180 = st_makevalid(st_simplifypreservetopology(geometry, 180)),
  geometry_80 = st_makevalid(st_simplifypreservetopology(geometry, 80)),
  geometry_50 = st_makevalid(st_simplifypreservetopology(geometry, 50)),
  geometry_40 = st_makevalid(st_simplifypreservetopology(geometry, 40))

Ну и отредактировал app.py, чтобы в запросе использовалось динамическое поле:

    # подобранные на глаз, значения сглаживания
    geometry_field = {
        1: 'geometry_60000',
        2: 'geometry_50000',
        3: 'geometry_25000',
        4: 'geometry_12000',
        5: 'geometry_6000',
        6: 'geometry_4000',
        7: 'geometry_1500',
        8: 'geometry_1000',
        9: 'geometry_350',
        10: 'geometry_180',
        11: 'geometry_80',
        12: 'geometry_50',
        13: 'geometry_40',
    }.get(zoom, 'geometry')

    # наш запрос
    q = db.Model.raw("""
WITH regions AS (
    SELECT
        st_intersection({}, st_transform(st_geomfromtext(%s, 4326), 3857)) AS geometry,
        name as region_name
    FROM osm_boundaries
    WHERE admin_level = 4 and st_intersects(geometry, st_transform(st_geomfromtext(%s, 4326), 3857))
)
SELECT json_build_object(
          'type',   'Feature',
          'geometry', st_asgeojson(st_transform(geometry, 4326))::json,
          'properties', json_build_object(
            'name', region_name
          )
       ) as geometry
FROM regions
""".format(geometry_field), bounds_polygon, bounds_polygon)

Вообще, в идеале, стоит дополнительную геометрию хранить в отдельной таблице.

Ложка дегтя

Обработчик imposm не очень хорошо справляется с построением геометрии Чукотского А.О. если использовать только данные РФ. Изучив исходники, причиной оказалось отсутствие некоторых путей (way) на линии смены дат, из-за чего не удается построить корректную геометрию. И, как результат, на месте Чукотки пустота.

Возвращаем Чукотку на место

Скачиваем контур Чукотского А.О. и два пропущенных района, 151231, 1949881, 1949879 - это идентификаторы relation в OSM базе:

wget https://www.openstreetmap.org/api/0.6/relation/151231/full -O chu.xml
wget https://www.openstreetmap.org/api/0.6/relation/1949881/full -O chu2.xml
wget https://www.openstreetmap.org/api/0.6/relation/1949879/full -O chu3.xml

Создаем временную БД:

createdb osm_data_test

Заносим во временную БД информацию по региону с помощью утилиты ogr2ogr (которую можно установить вместе с пакетом gdal-bin):

ogr2ogr -f 'PostgreSQL' PG:"host=localhost user=user password=*** dbname=osm_data_test" chu.xml
ogr2ogr -f 'PostgreSQL' PG:"host=localhost user=user password=*** dbname=osm_data_test" chu2.xml
ogr2ogr -f 'PostgreSQL' PG:"host=localhost user=user password=*** dbname=osm_data_test" chu3.xml

Подключаемся к БД osm_data_test, там будет регион со сформированной геометрией:

SELECT osm_id, wkb_geometry, boundary, name, admin_level, other_tags
FROM multipolygons

Если еще не создали расширение postgis, то сейчас самое время:

CREATE EXTENSION postgis;

Формируем COPY дамп в какой-нибудь временный файл:

COPY (SELECT osm_id,
        boundary,
        name,
        admin_level,
        other_tags,
        st_transform(
            ST_GeomFromWKB(wkb_geometry, 4326),
            3857
        ) FROM multipolygons
)  TO '/tmp/chu_out.sql';

Теперь подключаемся к основной базе geoadmin, где лежат все наши границы, сохраняем, и заносим информацию по Чукотке в таблицу osm_boundaries:

COPY osm_boundaries(osm_id, boundary, name, admin_level, hstore_tags, geometry) FROM '/tmp/chu_out.sql'

Вот и все, до новых встреч!