From 2ecaeee50a44f3aecd66831f2c5622d339ae6527 Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Sat, 8 Dec 2018 17:34:16 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=BE=D0=BD=D1=82=D0=B5=D0=B9=D0=BD?= =?UTF-8?q?=D0=B5=D1=80=20=D1=81=20=D0=BF=D0=B0=D1=82=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=D0=BC=20imposm3=20=D0=B8=20=D1=81=D0=BA=D1=80=D0=B8?= =?UTF-8?q?=D0=BF=D1=82=D0=BE=D0=BC=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7?= =?UTF-8?q?=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 36 ++++++ README.md | 13 ++ etc/apt/apt.conf | 4 + home/imposm3-all.yml | 82 +++++++++++++ home/osm-loader.pl | 283 +++++++++++++++++++++++++++++++++++++++++++ split-ways.diff | 222 +++++++++++++++++++++++++++++++++ 6 files changed, 640 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 etc/apt/apt.conf create mode 100644 home/imposm3-all.yml create mode 100755 home/osm-loader.pl create mode 100644 split-ways.diff diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..66e9553 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +FROM debian:sid AS build +MAINTAINER Vitaliy Filippov + +ADD etc /etc + +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y -q \ + less wget sudo curl unzip git mc ca-certificates \ + build-essential golang-go libleveldb-dev libgeos-dev && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +ADD split-ways.diff /root/ + +# Специальный патченый imposm3 +RUN cd /root && git clone https://github.com/omniscale/imposm3/ go/src/github.com/omniscale/imposm3 && \ + cd go/src/github.com/omniscale/imposm3 && git checkout c87a9a2b5761787a637e0fa73df9d4331846587d && \ + patch -p1 < /root/split-ways.diff && make build + +RUN dpkg -l | grep libgeos | awk '{print $2}' >> /root/pkg.lst + +FROM debian:sid AS run +MAINTAINER Vitaliy Filippov + +ADD etc /etc + +COPY --from=build /root/pkg.lst /root/ + +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y -q \ + less wget sudo curl unzip git mc \ + `cat /root/pkg.lst` libleveldb1v5 libdbd-pg-perl && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +COPY --from=build /root/go/src/github.com/omniscale/imposm3/imposm /usr/bin/imposm3 + +ADD home /home + +CMD /home/osm_loader.pl diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e6116a --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +## Building + +``` +docker build -t imposm3 . +``` + +## Running + +``` +docker run -it -d --restart always \ + -v /home/test/russia-latest.osm.pbf:/home/valhalla/data.osm.pbf \ + --name valhalla -p 8002:8002 valhalla +``` diff --git a/etc/apt/apt.conf b/etc/apt/apt.conf new file mode 100644 index 0000000..68d79bf --- /dev/null +++ b/etc/apt/apt.conf @@ -0,0 +1,4 @@ +APT::Install-Suggests false; +APT::Install-Recommends false; +Acquire::Retries 5; +Binary::apt::APT::Keep-Downloaded-Packages "false"; diff --git a/home/imposm3-all.yml b/home/imposm3-all.yml new file mode 100644 index 0000000..0aec770 --- /dev/null +++ b/home/imposm3-all.yml @@ -0,0 +1,82 @@ +# Нужно применять с патченым imposm3 (патч тут же, imposm3-split-ways.diff) +# Иначе, несмотря на area_tags и linear_tags, здания оказываются в linestring + +areas: + area_tags: [building, landuse, leisure, natural, aeroway] + linear_tags: [highway, barrier] + +tags: + load_all: true + exclude: + - created_by + - source + +tables: + point: + type: point + fields: + - name: osm_id + type: id + - name: geometry + type: geometry + - name: tags + type: hstore_tags + mapping: + __any__: + - __any__ + linestring: + type: linestring + fields: + - name: osm_id + type: id + - name: geometry + type: geometry + - name: tags + type: hstore_tags + mapping: + __any__: + - __any__ + polygon: + type: polygon + fields: + - name: osm_id + type: id + - name: geometry + type: geometry + - name: tags + type: hstore_tags + mapping: + __any__: + - __any__ + relation_member: + type: relation_member + fields: + - name: osm_id + type: id + - name: member + type: member_id + - name: index + type: member_index + - name: type + type: member_type + - name: role + type: member_role + mapping: + __any__: + - __any__ + relation: + type: relation + fields: + - name: osm_id + type: id + - key: type + name: type + type: string + - key: name + name: name + type: string + - name: tags + type: hstore_tags + mapping: + __any__: + - __any__ diff --git a/home/osm-loader.pl b/home/osm-loader.pl new file mode 100755 index 0000000..bfb65e7 --- /dev/null +++ b/home/osm-loader.pl @@ -0,0 +1,283 @@ +#!/usr/bin/perl +# Configuration in env: OSM_CACHE_DIR, OSM_CARTO_VERSION, URL_LATEST, URL_UPDATES, +# plus PG_ENV_OSM_{DB,HOST,PORT,USER,PASSWORD} like in README for the render server itself + +use strict; +use DBI; +use POSIX; + +my $dbh; +my $dir = $ENV{OSM_CACHE_DIR} || '/var/lib/mod_tile/downloads'; +my $method = $ENV{OSM_METHOD} || 'osm2pgsql-carto'; +my $url_latest = $ENV{URL_LATEST} || 'http://download.geofabrik.de/russia-latest.osm.pbf'; +my $url_updates = $ENV{URL_UPDATES} || 'http://download.geofabrik.de/russia-updates'; + +if (!$ENV{PG_ENV_OSM_DB}) +{ + fatal("OSM database not specified"); +} + +info("Started OSM update for database: $ENV{PG_ENV_OSM_DB}"); + +if ($method ne 'osm2pgsql-carto' && $method ne 'imposm3-all') +{ + fatal("Incorrect OSM update mode: $method"); +} + +-e $dir || mkdir($dir); +chdir $dir or fatal("Failed to chdir $dir"); +eval { run_update() }; +if ($@) +{ + fatal("$@"); +} +exit(0); + +sub run_update +{ + my $state = parse_geofabrik_state($url_updates, $dir); + my $dbh = DBI->connect( + 'dbi:Pg:dbname='.$ENV{PG_ENV_OSM_DB}.';host='.$ENV{PG_ENV_OSM_HOST}.';port='.($ENV{PG_ENV_OSM_PORT}||5432), + $ENV{PG_ENV_OSM_USER}, $ENV{PG_ENV_OSM_PASSWORD}, {AutoCommit => 0, RaiseError => 1} + ); + my ($version) = eval { $dbh->selectrow_array( + 'SELECT value FROM replication_state WHERE name=? FOR UPDATE', {}, 'osm_version' + ) }; + if (!$version) + { + if (!$ENV{OSM_INIT}) + { + fatal("Current OSM version missing, run with OSM_INIT=1 environment variable to initialize"); + } + $dbh->rollback; # иначе postgresql пишет "statements until the end ignored" + # создаём таблицу и оставляем её заблокированной + init_state($dbh, $state); + $dbh->do('CREATE EXTENSION IF NOT EXISTS postgis'); + $dbh->do('CREATE EXTENSION IF NOT EXISTS hstore'); + # качаем дамп + my ($fn) = $url_latest =~ /([^\/]+)$/so; + $fn =~ s/^([^\.]+)/$1-$state->{timestamp}/; + info("Downloading $url_latest"); + system("curl -s -C - -f '$url_latest' -o $dir/$fn"); + if ($? || !-e "$dir/$fn") + { + fatal("Failed to download $url_latest"); + } + if ($method eq 'osm2pgsql-carto') + { + init_osm2pgsql($dbh, $state->{timestamp} . ' ' . $state->{sequenceNumber}, "$dir/$fn"); + } + else + { + init_imposm3($dbh, $state->{timestamp} . ' ' . $state->{sequenceNumber}, "$dir/$fn"); + } + $dbh->commit; + info("OSM database initialized with version: ".$state->{timestamp}); + } + else + { + my $apply = load_geofabrik_deltas($version, $state, $url_updates, $dir); + chdir($dir); + if ($method eq 'osm2pgsql-carto') + { + apply_deltas_osm2pgsql($apply); + } + else + { + apply_deltas_imposm3($apply); + } + update_state($dbh, $state); + $dbh->commit; + info("OSM database updated to version: ".$state->{timestamp}); + if ($ENV{OSM_EXPIRE}) + { + system("cat $dir/expire.list | render_expired --map=osm_carto --touch-from=11"); + system("cat $dir/expire.list | render_expired --map=osm_bright --touch-from=11"); + } + } +} + +sub parse_geofabrik_state +{ + my ($url_updates, $dir) = @_; + system("curl -s -f '$url_updates/state.txt' -o $dir/state.txt"); + if (!-r "$dir/state.txt") + { + fatal("Error downloading $url_updates/state.txt"); + } + else + { + my $state; + if (open FD, "<$dir/state.txt") + { + local $/ = undef; + $state = ; + close FD; + $state = { map { (split /\s*=\s*/, $_, 2) } grep { !/^\s*(#.*)?$/so && /=/so } split /\n/, $state }; + if (!$state->{timestamp} || !$state->{sequenceNumber}) + { + fatal("State file incorrect, should have timestamp= and sequenceNumber="); + } + $state->{timestamp} =~ s/\\//g; + return $state; + } + } + fatal("Error downloading $url_updates/state.txt"); +} + +sub load_geofabrik_deltas +{ + my ($version, $state, $url_updates, $dir) = @_; + my ($timestamp, $i) = split /\s+/, $version; + my $apply = []; + while ($i <= $state->{sequenceNumber}) + { + my $subdir = sprintf("%03d/%03d", $i / 1000000, ($i / 1000) % 1000); + my $fn = sprintf("%03d.osc.gz", $i % 1000); + info("Downloading $url_updates/$subdir/$fn"); + system("mkdir -p $dir/$subdir && curl -C - -s -f '$url_updates/$subdir/$fn' -o $dir/$subdir/$fn"); + if (-e "$dir/$subdir/$fn") + { + push @$apply, "$subdir/$fn"; + } + else + { + fatal("Delta not available: $url_updates/$subdir/$fn\n"); + } + $i++; + } + return $apply; +} + +sub init_state +{ + my ($dbh, $state) = @_; + $dbh->do('CREATE TABLE IF NOT EXISTS replication_state (name varchar(1024) not null primary key, value text not null)'); + $dbh->do( + 'INSERT INTO replication_state (name, value) VALUES (?, ?)', + {}, 'osm_version', $state->{timestamp} . ' ' . $state->{sequenceNumber} + ); +} + +sub update_state +{ + my ($dbh, $state) = @_; + $dbh->do( + 'UPDATE replication_state SET value=? WHERE name=?', + {}, $state->{timestamp} . ' ' . $state->{sequenceNumber}, 'osm_version' + ); +} + +sub init_osm2pgsql +{ + my ($dbh, $path) = @_; + my $carto_dir = '/usr/share/mapnik/openstreetmap-carto-'.$ENV{OSM_CARTO_VERSION}; + if (!$ENV{OSM_CARTO_VERSION}) + { + fatal("osm-carto path not specified"); + } + my $cmd = + "PGPASSWORD='".$ENV{PG_ENV_OSM_PASSWORD}."' osm2pgsql -I -s -c --hstore". + " --style $carto_dir/openstreetmap-carto.style". + " --tag-transform-script $carto_dir/openstreetmap-carto.lua". + " -C 4000 -G -H '".$ENV{PG_ENV_OSM_HOST}."' -U '".$ENV{PG_ENV_OSM_USER}."' -d '".$ENV{PG_ENV_OSM_DB}."'". + " -P ".($ENV{PG_ENV_OSM_PORT} || 5432)." '$path'"; + system($cmd); + if ($?) + { + fatal("$cmd failed"); + } + local $/ = undef; + my $fd; + open $fd, "$carto_dir/indexes.sql"; + for my $index (split /;\s*/, <$fd>) + { + $dbh->do($index); + } + close $fd; +} + +sub init_imposm3 +{ + my ($dbh, $path) = @_; + my $cmd = + "imposm3 import -connection 'postgis://".$ENV{PG_ENV_OSM_USER}.":".$ENV{PG_ENV_OSM_PASSWORD}. + "@".$ENV{PG_ENV_OSM_HOST}.(($ENV{PG_ENV_OSM_PORT}||5432) != 5432 ? ":".$ENV{PG_ENV_OSM_PORT} : ""). + "/".$ENV{PG_ENV_OSM_DB}."' -cachedir '".$ENV{OSM_CACHE_DIR}."/imposm3-cache' -mapping '/home/imposm3-all.yml' -srid 4326 -diff". + " -read '$path' -write"; + system($cmd); + if ($?) + { + fatal("$cmd failed"); + } + my $indexes = "SET SEARCH_PATH TO import, public; +CREATE INDEX IF NOT EXISTS osm_polygon_area ON osm_polygon (st_area(geometry)); +CREATE INDEX IF NOT EXISTS osm_point_tags ON osm_point USING gin (tags); +CREATE INDEX IF NOT EXISTS osm_linestring_tags ON osm_linestring USING gin (tags); +CREATE INDEX IF NOT EXISTS osm_polygon_tags ON osm_polygon USING gin (tags); +CREATE INDEX IF NOT EXISTS osm_relation_tags ON osm_relation USING gin (tags); +CREATE INDEX IF NOT EXISTS osm_point_text ON osm_point USING gin (to_tsvector('russian', tags::text)); +CREATE INDEX IF NOT EXISTS osm_linestring_text ON osm_linestring USING gin (to_tsvector('russian', tags::text)); +CREATE INDEX IF NOT EXISTS osm_polygon_text ON osm_polygon USING gin (to_tsvector('russian', tags::text)); +CREATE INDEX IF NOT EXISTS osm_relation_text ON osm_relation USING gin (to_tsvector('russian', tags::text))"; + foreach my $index (split /;\n/, $indexes) + { + $dbh->do($index); + } +} + +sub apply_deltas_osm2pgsql +{ + my ($apply, $carto_dir) = @_; + if (@$apply) + { + my $cmd = + "PGPASSWORD='".$ENV{PG_ENV_OSM_PASSWORD}."' osm2pgsql --append -e15 -o '".$ENV{OSM_CACHE_DIR}."/expire.list' -I -s --hstore". + " --style $carto_dir/openstreetmap-carto.style". + " --tag-transform-script $carto_dir/openstreetmap-carto.lua". + " -C 4000 -G -H '".$ENV{PG_ENV_OSM_HOST}."' -U '".$ENV{PG_ENV_OSM_USER}."' -d '".$ENV{PG_ENV_OSM_DB}."'". + " -P ".($ENV{PG_ENV_OSM_PORT} || 5432)." '".join("' '", @$apply)."'"; + system($cmd); + if ($?) + { + fatal("$cmd failed"); + } + } +} + +sub apply_deltas_imposm3 +{ + my ($apply) = @_; + my $carto_dir = '/usr/share/mapnik/openstreetmap-carto-'.$ENV{OSM_CARTO_VERSION}; + if (!$ENV{OSM_CARTO_VERSION}) + { + fatal("osm-carto path not specified"); + } + if (@$apply) + { + my $cmd = + "imposm3 diff -connection 'postgis://".$ENV{PG_ENV_OSM_USER}.":".$ENV{PG_ENV_OSM_PASSWORD}. + "@".$ENV{PG_ENV_OSM_HOST}.(($ENV{PG_ENV_OSM_PORT}||5432) != 5432 ? ":".$ENV{PG_ENV_OSM_PORT} : ""). + "/".$ENV{PG_ENV_OSM_DB}."' -cachedir '".$ENV{OSM_CACHE_DIR}."/imposm3-cache' -mapping '/home/imposm3-all.yml' -srid 4326". + " '".join("' '", @$apply)."'"; + system($cmd); + if ($?) + { + fatal("$cmd failed"); + } + } +} + +sub info +{ + my ($msg) = @_; + print POSIX::strftime("%Y-%m-%d %H:%M:%S", localtime)." [info] $msg\n"; +} + +sub fatal +{ + my ($msg) = @_; + eval { $dbh->rollback } if $dbh; + print POSIX::strftime("%Y-%m-%d %H:%M:%S", localtime)." [error] $msg\n"; + exit(1); +} diff --git a/split-ways.diff b/split-ways.diff new file mode 100644 index 0000000..214fcc6 --- /dev/null +++ b/split-ways.diff @@ -0,0 +1,222 @@ +commit 8e1da67b3d4221aa16469cba53c8097ddbe78cb8 +Author: Vitaliy Filippov +Date: Sat Sep 29 02:47:19 2018 +0300 + + Split ways between linestrings / polygons correctly (so one object cannot be a linestring and a polygon at the same time) + +diff --git a/mapping/mapping.go b/mapping/mapping.go +index 4f48658..7e58a93 100644 +--- a/mapping/mapping.go ++++ b/mapping/mapping.go +@@ -275,7 +275,7 @@ type elementFilter func(tags element.Tags, key Key, closed bool) bool + + type tableElementFilters map[string][]elementFilter + +-func (m *Mapping) addTypedFilters(tableType TableType, filters tableElementFilters) { ++func (m *Mapping) getAreaTags() (map[Key]struct{}, map[Key]struct{}) { + var areaTags map[Key]struct{} + var linearTags map[Key]struct{} + if m.Conf.Areas.AreaTags != nil { +@@ -290,42 +290,7 @@ func (m *Mapping) addTypedFilters(tableType TableType, filters tableElementFilte + linearTags[Key(tag)] = struct{}{} + } + } +- +- for name, t := range m.Conf.Tables { +- if TableType(t.Type) != GeometryTable && TableType(t.Type) != tableType { +- continue +- } +- if TableType(t.Type) == LineStringTable && areaTags != nil { +- f := func(tags element.Tags, key Key, closed bool) bool { +- if closed { +- if tags["area"] == "yes" { +- return false +- } +- if tags["area"] != "no" { +- if _, ok := areaTags[key]; ok { +- return false +- } +- } +- } +- return true +- } +- filters[name] = append(filters[name], f) +- } +- if TableType(t.Type) == PolygonTable && linearTags != nil { +- f := func(tags element.Tags, key Key, closed bool) bool { +- if closed && tags["area"] == "no" { +- return false +- } +- if tags["area"] != "yes" { +- if _, ok := linearTags[key]; ok { +- return false +- } +- } +- return true +- } +- filters[name] = append(filters[name], f) +- } +- } ++ return areaTags, linearTags + } + + func (m *Mapping) addRelationFilters(tableType TableType, filters tableElementFilters) { +diff --git a/mapping/matcher.go b/mapping/matcher.go +index 2c58446..346782c 100644 +--- a/mapping/matcher.go ++++ b/mapping/matcher.go +@@ -10,13 +10,11 @@ func (m *Mapping) pointMatcher() (NodeMatcher, error) { + m.mappings(PointTable, mappings) + filters := make(tableElementFilters) + m.addFilters(filters) +- m.addTypedFilters(PointTable, filters) + tables, err := m.tables(PointTable) + return &tagMatcher{ + mappings: mappings, + filters: filters, + tables: tables, +- matchAreas: false, + }, err + } + +@@ -25,12 +23,15 @@ func (m *Mapping) lineStringMatcher() (WayMatcher, error) { + m.mappings(LineStringTable, mappings) + filters := make(tableElementFilters) + m.addFilters(filters) +- m.addTypedFilters(LineStringTable, filters) ++ areaTags, linearTags := m.getAreaTags() + tables, err := m.tables(LineStringTable) + return &tagMatcher{ + mappings: mappings, + filters: filters, + tables: tables, ++ areaTags: areaTags, ++ linearTags: linearTags, ++ matchLines: true, + matchAreas: false, + }, err + } +@@ -40,7 +41,7 @@ func (m *Mapping) polygonMatcher() (RelWayMatcher, error) { + m.mappings(PolygonTable, mappings) + filters := make(tableElementFilters) + m.addFilters(filters) +- m.addTypedFilters(PolygonTable, filters) ++ areaTags, linearTags := m.getAreaTags() + relFilters := make(tableElementFilters) + m.addRelationFilters(PolygonTable, relFilters) + tables, err := m.tables(PolygonTable) +@@ -49,6 +50,9 @@ func (m *Mapping) polygonMatcher() (RelWayMatcher, error) { + filters: filters, + tables: tables, + relFilters: relFilters, ++ areaTags: areaTags, ++ linearTags: linearTags, ++ matchLines: false, + matchAreas: true, + }, err + } +@@ -58,8 +62,6 @@ func (m *Mapping) relationMatcher() (RelationMatcher, error) { + m.mappings(RelationTable, mappings) + filters := make(tableElementFilters) + m.addFilters(filters) +- m.addTypedFilters(PolygonTable, filters) +- m.addTypedFilters(RelationTable, filters) + relFilters := make(tableElementFilters) + m.addRelationFilters(RelationTable, relFilters) + tables, err := m.tables(RelationTable) +@@ -68,7 +70,6 @@ func (m *Mapping) relationMatcher() (RelationMatcher, error) { + filters: filters, + tables: tables, + relFilters: relFilters, +- matchAreas: true, + }, err + } + +@@ -77,7 +78,6 @@ func (m *Mapping) relationMemberMatcher() (RelationMatcher, error) { + m.mappings(RelationMemberTable, mappings) + filters := make(tableElementFilters) + m.addFilters(filters) +- m.addTypedFilters(RelationMemberTable, filters) + relFilters := make(tableElementFilters) + m.addRelationFilters(RelationMemberTable, relFilters) + tables, err := m.tables(RelationMemberTable) +@@ -86,7 +86,6 @@ func (m *Mapping) relationMemberMatcher() (RelationMatcher, error) { + filters: filters, + tables: tables, + relFilters: relFilters, +- matchAreas: true, + }, err + } + +@@ -127,6 +126,9 @@ type tagMatcher struct { + tables map[string]*rowBuilder + filters tableElementFilters + relFilters tableElementFilters ++ areaTags map[Key]struct{} ++ linearTags map[Key]struct{} ++ matchLines bool + matchAreas bool + } + +@@ -135,23 +137,7 @@ func (tm *tagMatcher) MatchNode(node *element.Node) []Match { + } + + func (tm *tagMatcher) MatchWay(way *element.Way) []Match { +- if tm.matchAreas { // match way as polygon +- if way.IsClosed() { +- if way.Tags["area"] == "no" { +- return nil +- } +- return tm.match(way.Tags, true, false) +- } +- } else { // match way as linestring +- if way.IsClosed() { +- if way.Tags["area"] == "yes" { +- return nil +- } +- return tm.match(way.Tags, true, false) +- } +- return tm.match(way.Tags, false, false) +- } +- return nil ++ return tm.match(way.Tags, way.IsClosed(), false) + } + + func (tm *tagMatcher) MatchRelation(rel *element.Relation) []Match { +@@ -205,7 +191,35 @@ func (tm *tagMatcher) match(tags element.Tags, closed bool, relation bool) []Mat + for t, match := range tables { + filters, ok := tm.filters[t.Name] + filteredOut := false +- if ok { ++ if !relation && (tm.matchLines || tm.matchAreas) { ++ if !closed { ++ // open way is always a linestring ++ filteredOut = tm.matchLines == false ++ } else { ++ // allow to include closed ways as linestrings if explicitly marked ++ // default -> area ++ // area=no or linear tags -> line ++ // area=yes or area tags -> area ++ filteredOut = tm.matchAreas && tags["area"] == "no" || !tm.matchAreas && tags["area"] != "no" ++ for k, _ := range tm.linearTags { ++ if _, ok := tags[string(k)]; ok { ++ filteredOut = tm.matchAreas ++ break ++ } ++ } ++ // but area=yes or area tag means it shouldn't be a linestring ++ if tags["area"] == "yes" { ++ filteredOut = tm.matchAreas ++ } ++ for k, _ := range tm.areaTags { ++ if _, ok := tags[string(k)]; ok { ++ filteredOut = !tm.matchAreas ++ break ++ } ++ } ++ } ++ } ++ if ok && !filteredOut { + for _, filter := range filters { + if !filter(tags, Key(match.Key), closed) { + filteredOut = true