]> git.openstreetmap.org Git - nominatim.git/commitdiff
Merge remote-tracking branch 'upstream/master'
authorSarah Hoffmann <lonvia@denofr.de>
Tue, 1 Dec 2020 15:42:01 +0000 (16:42 +0100)
committerSarah Hoffmann <lonvia@denofr.de>
Tue, 1 Dec 2020 15:42:01 +0000 (16:42 +0100)
docs/admin/Migration.md
nominatim/indexer/progress.py
sql/functions/address_lookup.sql
test/bdd/db/import/addressing.feature

index 9b6395e6a3e278ca1a31c6f582f8d4d7bb564c5d..21bbb51ae3e14b422382dc71c1edcd201a4dd0c7 100644 (file)
@@ -11,7 +11,7 @@ SQL statements should be executed from the PostgreSQL commandline. Execute
 ### Change of layout of search_name_* tables
 
 The table need a different index for nearest place lookup. Recreate the
-indexs suing the following shell script:
+indexes using the following shell script:
 
 ```bash
 for table in `psql -d nominatim -c "SELECT tablename FROM pg_tables WHERE tablename LIKE 'search_name_%'" -tA | grep -v search_name_blank`;
@@ -39,13 +39,15 @@ which needs a different database index. Create it with the following SQL command
 
 ```sql
 CREATE INDEX idx_placex_pendingsector_rank_address
-  ON placex USING BTREE (rank_address, geometry_sector) where indexed_status > 0;
+  ON placex
+  USING BTREE (rank_address, geometry_sector)
+  WHERE indexed_status > 0;
 ```
 
 You can then drop the old index with:
 
 ```sql
-DROP INDEX idx_placex_pendingsector
+DROP INDEX idx_placex_pendingsector;
 ```
 
 ### Unused index
@@ -53,7 +55,7 @@ DROP INDEX idx_placex_pendingsector
 This index has been unused ever since the query using it was changed two years ago. Saves about 12GB on a planet installation.
 
 ```sql
-DROP INDEX idx_placex_geometry_reverse_lookupPoint
+DROP INDEX idx_placex_geometry_reverse_lookupPoint;
 ```
 
 ### Switching to dotenv
@@ -78,10 +80,14 @@ follows:
   * reimport the tables: `./utils/setup.php --import-wikipedia-articles`
   * update the functions: `./utils/setup.php --create-functions --enable-diff-updates`
   * create a new lookup index:
-```
-CREATE INDEX idx_placex_wikidata on placex
-USING BTREE ((extratags -> 'wikidata'))
-WHERE extratags ? 'wikidata' and class = 'place' and osm_type = 'N' and rank_search < 26
+```sql
+CREATE INDEX idx_placex_wikidata
+  ON placex
+  USING BTREE ((extratags -> 'wikidata'))
+  WHERE extratags ? 'wikidata'
+    AND class = 'place'
+    AND osm_type = 'N'
+    AND rank_search < 26;
 ```
   * compute importance: `./utils/update.php --recompute-importance`
 
@@ -138,7 +144,7 @@ The new format is
 
 ### Natural Earth country boundaries no longer needed as fallback
 
-```
+```sql
 DROP TABLE country_naturalearthdata;
 ```
 
@@ -164,27 +170,37 @@ following command:
 The reverse algorithm has changed and requires new indexes. Run the following
 SQL statements to create the indexes:
 
-```
+```sql
 CREATE INDEX idx_placex_geometry_reverse_lookupPoint
-  ON placex USING gist (geometry)
-  WHERE (name is not null or housenumber is not null or rank_address between 26 and 27)
-    AND class not in ('railway','tunnel','bridge','man_made')
-    AND rank_address >= 26 AND indexed_status = 0 AND linked_place_id is null;
+  ON placex
+  USING gist (geometry)
+  WHERE (name IS NOT null or housenumber IS NOT null or rank_address BETWEEN 26 AND 27)
+    AND class NOT IN ('railway','tunnel','bridge','man_made')
+    AND rank_address >= 26
+    AND indexed_status = 0
+    AND linked_place_id IS null;
 CREATE INDEX idx_placex_geometry_reverse_lookupPolygon
   ON placex USING gist (geometry)
   WHERE St_GeometryType(geometry) in ('ST_Polygon', 'ST_MultiPolygon')
-    AND rank_address between 4 and 25 AND type != 'postcode'
-    AND name is not null AND indexed_status = 0 AND linked_place_id is null;
+    AND rank_address between 4 and 25
+    AND type != 'postcode'
+    AND name is not null
+    AND indexed_status = 0
+    AND linked_place_id is null;
 CREATE INDEX idx_placex_geometry_reverse_placeNode
   ON placex USING gist (geometry)
-  WHERE osm_type = 'N' AND rank_search between 5 and 25
-    AND class = 'place' AND type != 'postcode'
-    AND name is not null AND indexed_status = 0 AND linked_place_id is null;
+  WHERE osm_type = 'N'
+    AND rank_search between 5 and 25
+    AND class = 'place'
+    AND type != 'postcode'
+    AND name is not null
+    AND indexed_status = 0
+    AND linked_place_id is null;
 ```
 
 You also need to grant the website user access to the `country_osm_grid` table:
 
-```
+```sql
 GRANT SELECT ON table country_osm_grid to "www-user";
 ```
 
@@ -192,7 +208,7 @@ Replace the `www-user` with the user name of your website server if necessary.
 
 You can now drop the unused indexes:
 
-```
+```sql
 DROP INDEX idx_placex_reverse_geometry;
 ```
 
@@ -221,8 +237,8 @@ CREATE INDEX idx_postcode_geometry ON location_postcode USING GIST (geometry);
 CREATE UNIQUE INDEX idx_postcode_id ON location_postcode USING BTREE (place_id);
 CREATE INDEX idx_postcode_postcode ON location_postcode USING BTREE (postcode);
 GRANT SELECT ON location_postcode TO "www-data";
-drop type if exists nearfeaturecentr cascade;
-create type nearfeaturecentr as (
+DROP TYPE IF EXISTS nearfeaturecentr CASCADE;
+CREATE TYPE nearfeaturecentr AS (
   place_id BIGINT,
   keywords int[],
   rank_address smallint,
index 456d3eae08aeff9258cc4446f709241451070563..99120673faa67680216ac5fc48d6c8f93da62d03 100644 (file)
@@ -2,13 +2,17 @@
 #
 # This file is part of Nominatim.
 # Copyright (C) 2020 Sarah Hoffmann
-
+"""
+Helpers for progress logging.
+"""
 import logging
 from datetime import datetime
 
-log = logging.getLogger()
+LOG = logging.getLogger()
+
+INITIAL_PROGRESS = 10
 
-class ProgressLogger(object):
+class ProgressLogger:
     """ Tracks and prints progress for the indexing process.
         `name` is the name of the indexing step being tracked.
         `total` sets up the total number of items that need processing.
@@ -21,32 +25,40 @@ class ProgressLogger(object):
         self.total_places = total
         self.done_places = 0
         self.rank_start_time = datetime.now()
-        self.next_info = 100 if log.isEnabledFor(logging.INFO) else total + 1
+        self.log_interval = log_interval
+        self.next_info = INITIAL_PROGRESS if LOG.isEnabledFor(logging.INFO) else total + 1
 
     def add(self, num=1):
         """ Mark `num` places as processed. Print a log message if the
-            logging is at least info and the log interval has past.
+            logging is at least info and the log interval has passed.
         """
         self.done_places += num
 
-        if self.done_places >= self.next_info:
-            now = datetime.now()
-            done_time = (now - self.rank_start_time).total_seconds()
-            places_per_sec = self.done_places / done_time
-            eta = (self.total_places - self.done_places)/places_per_sec
+        if self.done_places < self.next_info:
+            return
+
+        now = datetime.now()
+        done_time = (now - self.rank_start_time).total_seconds()
+
+        if done_time < 2:
+            self.next_info = self.done_places + INITIAL_PROGRESS
+            return
+
+        places_per_sec = self.done_places / done_time
+        eta = (self.total_places - self.done_places) / places_per_sec
 
-            log.info("Done {} in {} @ {:.3f} per second - {} ETA (seconds): {:.2f}"
-                     .format(self.done_places, int(done_time),
-                             places_per_sec, self.name, eta))
+        LOG.info("Done %d in %d @ %.3f per second - %s ETA (seconds): %.2f",
+                 self.done_places, int(done_time),
+                 places_per_sec, self.name, eta)
 
-            self.next_info += int(places_per_sec)
+        self.next_info += int(places_per_sec) * self.log_interval
 
     def done(self):
-        """ Print final staticstics about the progress.
+        """ Print final statistics about the progress.
         """
         rank_end_time = datetime.now()
         diff_seconds = (rank_end_time-self.rank_start_time).total_seconds()
 
-        log.warning("Done {}/{} in {} @ {:.3f} per second - FINISHED {}\n".format(
+        LOG.warning("Done %d/%d in %d @ %.3f per second - FINISHED %s\n",
                     self.done_places, self.total_places, int(diff_seconds),
-                    self.done_places/diff_seconds, self.name))
+                    self.done_places/diff_seconds, self.name)
index 2426a698f03c4dc3cb373ff793fa9e1eb364e6e7..c1a7b7a4fffcb0e555cc1bfbf5de7cef1516bafe 100644 (file)
@@ -87,92 +87,90 @@ CREATE OR REPLACE FUNCTION get_addressdata(in_place_id BIGINT, in_housenumber IN
   RETURNS setof addressline
   AS $$
 DECLARE
-  for_place_id BIGINT;
-  result TEXT[];
-  search TEXT[];
-  current_rank_address INTEGER;
+  place RECORD;
   location RECORD;
-  countrylocation RECORD;
-  searchcountrycode varchar(2);
-  searchhousenumber TEXT;
-  searchhousename HSTORE;
-  searchpostcode TEXT;
-  postcode_isexact BOOL;
-  searchclass TEXT;
-  searchtype TEXT;
-  search_unlisted_place TEXT;
-  countryname HSTORE;
+  current_rank_address INTEGER;
+  location_isaddress BOOLEAN;
 BEGIN
   -- The place in question might not have a direct entry in place_addressline.
-  -- Look for the parent of such places then and save if in for_place_id.
-
-  postcode_isexact := false;
+  -- Look for the parent of such places then and save it in place.
 
   -- first query osmline (interpolation lines)
   IF in_housenumber >= 0 THEN
-    SELECT parent_place_id, country_code, in_housenumber::text, postcode,
-           null, 'place', 'house'
+    SELECT parent_place_id as place_id, country_code,
+           in_housenumber::text as housenumber, postcode,
+           'place' as class, 'house' as type,
+           null::hstore as name, null::hstore as address,
+           ST_Centroid(linegeo) as centroid
+      INTO place
       FROM location_property_osmline
-      WHERE place_id = in_place_id AND in_housenumber>=startnumber
-            AND in_housenumber <= endnumber
-      INTO for_place_id, searchcountrycode, searchhousenumber,
-           searchpostcode, searchhousename, searchclass, searchtype;
+      WHERE place_id = in_place_id
+            AND in_housenumber between startnumber and endnumber;
   END IF;
 
   --then query tiger data
   -- %NOTIGERDATA% IF 0 THEN
-  IF for_place_id IS NULL AND in_housenumber >= 0 THEN
-    SELECT parent_place_id, 'us', in_housenumber::text, postcode, null,
-           'place', 'house'
+  IF place IS NULL AND in_housenumber >= 0 THEN
+    SELECT parent_place_id as place_id, 'us' as country_code,
+           in_housenumber::text as housenumber, postcode,
+           'place' as class, 'house' as type,
+           null::hstore as name, null::hstore as address,
+           ST_Centroid(linegeo) as centroid
+      INTO place
       FROM location_property_tiger
-      WHERE place_id = in_place_id AND in_housenumber >= startnumber
-            AND in_housenumber <= endnumber
-      INTO for_place_id, searchcountrycode, searchhousenumber,
-           searchpostcode, searchhousename, searchclass, searchtype;
+      WHERE place_id = in_place_id
+            AND in_housenumber between startnumber and endnumber;
   END IF;
   -- %NOTIGERDATA% END IF;
 
   -- %NOAUXDATA% IF 0 THEN
-  IF for_place_id IS NULL THEN
-    SELECT parent_place_id, 'us', housenumber, postcode, null, 'place', 'house'
+  IF place IS NULL THEN
+    SELECT parent_place_id as place_id, 'us' as country_code,
+           housenumber, postcode,
+           'place' as class, 'house' as type,
+           null::hstore as name, null::hstore as address,
+           centroid
+      INTO place
       FROM location_property_aux
-      WHERE place_id = in_place_id
-      INTO for_place_id,searchcountrycode, searchhousenumber,
-           searchpostcode, searchhousename, searchclass, searchtype;
+      WHERE place_id = in_place_id;
   END IF;
   -- %NOAUXDATA% END IF;
 
   -- postcode table
-  IF for_place_id IS NULL THEN
-    SELECT parent_place_id, country_code, postcode, 'place', 'postcode'
+  IF place IS NULL THEN
+    SELECT parent_place_id as place_id, country_code,
+           null::text as housenumber, postcode,
+           'place' as class, 'postcode' as type,
+           null::hstore as name, null::hstore as address,
+           null::geometry as centroid
+      INTO place
       FROM location_postcode
-      WHERE place_id = in_place_id
-      INTO for_place_id, searchcountrycode, searchpostcode,
-           searchclass, searchtype;
+      WHERE place_id = in_place_id;
   END IF;
 
   -- POI objects in the placex table
-  IF for_place_id IS NULL THEN
-    SELECT parent_place_id, country_code, housenumber,
-           postcode, address is not null and address ? 'postcode',
-           name, class, type,
-           address -> '_unlisted_place' as unlisted_place
+  IF place IS NULL THEN
+    SELECT parent_place_id as place_id, country_code,
+           housenumber, postcode,
+           class, type,
+           name, address,
+           centroid
+      INTO place
       FROM placex
-      WHERE place_id = in_place_id and rank_search > 27
-      INTO for_place_id, searchcountrycode, searchhousenumber,
-           searchpostcode, postcode_isexact, searchhousename, searchclass,
-           searchtype, search_unlisted_place;
+      WHERE place_id = in_place_id and rank_search > 27;
   END IF;
 
-  -- If for_place_id is still NULL at this point then the object has its own
+  -- If place is still NULL at this point then the object has its own
   -- entry in place_address line. However, still check if there is not linked
   -- place we should be using instead.
-  IF for_place_id IS NULL THEN
-    select coalesce(linked_place_id, place_id),  country_code,
+  IF place IS NULL THEN
+    select coalesce(linked_place_id, place_id) as place_id,  country_code,
            housenumber, postcode,
-           address is not null and address ? 'postcode', null
-      from placex where place_id = in_place_id
-      INTO for_place_id, searchcountrycode, searchhousenumber, searchpostcode, postcode_isexact, searchhousename;
+           class, type,
+           null::hstore as name, address,
+           null::geometry as centroid
+      INTO place
+      FROM placex where place_id = in_place_id;
   END IF;
 
 --RAISE WARNING '% % % %',searchcountrycode, searchhousenumber, searchpostcode;
@@ -183,28 +181,27 @@ BEGIN
     SELECT placex.place_id, osm_type, osm_id, name,
            coalesce(extratags->'linked_place', extratags->'place') as place_type,
            class, type, admin_level,
-           type not in ('postcode', 'postal_code') as isaddress,
            CASE WHEN rank_address = 0 THEN 100
                 WHEN rank_address = 11 THEN 5
                 ELSE rank_address END as rank_address,
-           0 as distance, country_code, postcode
+           country_code
       FROM placex
-      WHERE place_id = for_place_id
+      WHERE place_id = place.place_id
   LOOP
 --RAISE WARNING '%',location;
-    IF searchcountrycode IS NULL AND location.country_code IS NOT NULL THEN
-      searchcountrycode := location.country_code;
-    END IF;
     IF location.rank_address < 4 THEN
       -- no country locations for ranks higher than country
-      searchcountrycode := NULL;
+      place.country_code := NULL;
+    ELSEIF place.country_code IS NULL AND location.country_code IS NOT NULL THEN
+      place.country_code := location.country_code;
     END IF;
-    countrylocation := ROW(location.place_id, location.osm_type, location.osm_id,
-                           location.name, location.class, location.type,
-                           location.place_type,
-                           location.admin_level, true, location.isaddress,
-                           location.rank_address, location.distance)::addressline;
-    RETURN NEXT countrylocation;
+
+    RETURN NEXT ROW(location.place_id, location.osm_type, location.osm_id,
+                    location.name, location.class, location.type,
+                    location.place_type,
+                    location.admin_level, true,
+                    location.type not in ('postcode', 'postal_code'),
+                    location.rank_address, 0)::addressline;
 
     current_rank_address := location.rank_address;
   END LOOP;
@@ -218,82 +215,86 @@ BEGIN
            CASE WHEN rank_address = 11 THEN 5 ELSE rank_address END as rank_address,
            distance, country_code, postcode
       FROM place_addressline join placex on (address_place_id = placex.place_id)
-      WHERE place_addressline.place_id IN (for_place_id, in_place_id)
+      WHERE place_addressline.place_id IN (place.place_id, in_place_id)
             AND linked_place_id is null
-            AND (placex.country_code IS NULL OR searchcountrycode IS NULL
-                 OR placex.country_code = searchcountrycode)
-      ORDER BY rank_address desc, (place_addressline.place_id = in_place_id) desc,
+            AND (placex.country_code IS NULL OR place.country_code IS NULL
+                 OR placex.country_code = place.country_code)
+      ORDER BY rank_address desc,
+               (place_addressline.place_id = in_place_id) desc,
+               (fromarea and place.centroid is not null and not isaddress
+                and (place.address is null or avals(name) && avals(place.address))
+                and ST_Contains(geometry, place.centroid)) desc,
                isaddress desc, fromarea desc,
                distance asc, rank_search desc
   LOOP
     -- RAISE WARNING '%',location;
-    IF searchcountrycode IS NULL AND location.country_code IS NOT NULL THEN
-      searchcountrycode := location.country_code;
+    location_isaddress := location.rank_address != current_rank_address;
+
+    IF place.country_code IS NULL AND location.country_code IS NOT NULL THEN
+      place.country_code := location.country_code;
     END IF;
     IF location.type in ('postcode', 'postal_code')
-       AND searchpostcode is not null
+       AND place.postcode is not null
     THEN
       -- If the place had a postcode assigned, take this one only
       -- into consideration when it is an area and the place does not have
       -- a postcode itself.
-      IF location.fromarea AND not postcode_isexact AND location.isaddress THEN
-        searchpostcode := null; -- remove the less exact postcode
+      IF location.fromarea AND location.isaddress
+         AND (place.address is null or not place.address ? 'postcode')
+      THEN
+        place.postcode := null; -- remove the less exact postcode
       ELSE
-        location.isaddress := false;
+        location_isaddress := false;
       END IF;
     END IF;
-    countrylocation := ROW(location.place_id, location.osm_type, location.osm_id,
-                           location.name, location.class, location.type,
-                           location.place_type,
-                           location.admin_level, location.fromarea,
-                           location.isaddress and location.rank_address != current_rank_address,
-                           location.rank_address,
-                           location.distance)::addressline;
-    RETURN NEXT countrylocation;
-
-    IF location.isaddress THEN
-      current_rank_address := location.rank_address;
-    END IF;
+    RETURN NEXT ROW(location.place_id, location.osm_type, location.osm_id,
+                    location.name, location.class, location.type,
+                    location.place_type,
+                    location.admin_level, location.fromarea,
+                    location_isaddress,
+                    location.rank_address,
+                    location.distance)::addressline;
+
+    current_rank_address := location.rank_address;
   END LOOP;
 
   -- If no country was included yet, add the name information from country_name.
   IF current_rank_address > 4 THEN
-    SELECT name FROM country_name
-      WHERE country_code = searchcountrycode LIMIT 1 INTO countryname;
+    FOR location IN
+      SELECT name FROM country_name WHERE country_code = place.country_code LIMIT 1
+    LOOP
 --RAISE WARNING '% % %',current_rank_address,searchcountrycode,countryname;
-    IF countryname IS NOT NULL THEN
-      location := ROW(null, null, null, countryname, 'place', 'country', NULL,
+      RETURN NEXT ROW(null, null, null, location.name, 'place', 'country', NULL,
                       null, true, true, 4, 0)::addressline;
-      RETURN NEXT location;
-    END IF;
+    END LOOP;
   END IF;
 
   -- Finally add some artificial rows.
-  IF searchcountrycode IS NOT NULL THEN
-    location := ROW(null, null, null, hstore('ref', searchcountrycode),
+  IF place.country_code IS NOT NULL THEN
+    location := ROW(null, null, null, hstore('ref', place.country_code),
                     'place', 'country_code', null, null, true, false, 4, 0)::addressline;
     RETURN NEXT location;
   END IF;
 
-  IF searchhousename IS NOT NULL THEN
-    location := ROW(in_place_id, null, null, searchhousename, searchclass,
-                    searchtype, null, null, true, true, 29, 0)::addressline;
+  IF place.name IS NOT NULL THEN
+    location := ROW(in_place_id, null, null, place.name, place.class,
+                    place.type, null, null, true, true, 29, 0)::addressline;
     RETURN NEXT location;
   END IF;
 
-  IF searchhousenumber IS NOT NULL THEN
-    location := ROW(null, null, null, hstore('ref', searchhousenumber),
+  IF place.housenumber IS NOT NULL THEN
+    location := ROW(null, null, null, hstore('ref', place.housenumber),
                     'place', 'house_number', null, null, true, true, 28, 0)::addressline;
     RETURN NEXT location;
   END IF;
 
-  IF search_unlisted_place is not null THEN
-    RETURN NEXT ROW(null, null, null, hstore('name', search_unlisted_place),
+  IF place.address is not null and place.address ? '_unlisted_place' THEN
+    RETURN NEXT ROW(null, null, null, hstore('name', place.address->'_unlisted_place'),
                     'place', 'locality', null, null, true, true, 25, 0)::addressline;
   END IF;
 
-  IF searchpostcode IS NOT NULL THEN
-    location := ROW(null, null, null, hstore('ref', searchpostcode), 'place',
+  IF place.postcode is not null THEN
+    location := ROW(null, null, null, hstore('ref', place.postcode), 'place',
                     'postcode', null, null, false, true, 5, 0)::addressline;
     RETURN NEXT location;
   END IF;
index 9050c19b0923c6cf10d388e7725fd347d0847fa1..479ddd31d39f9011412d78ebeeb7591424628e14 100644 (file)
@@ -5,11 +5,11 @@ Feature: Address computation
     Scenario: place nodes are added to the address when they are close enough
         Given the 0.002 grid
             | 2 |  |  |  |  |  | 1 |  | 3 |
-        And the named places
-            | osm | class | type     | geometry |
-            | N1  | place | square   | 1 |
-            | N2  | place | hamlet   | 2 |
-            | N3  | place | hamlet   | 3 |
+        And the places
+            | osm | class | type     | name      | geometry |
+            | N1  | place | square   | Square    | 1 |
+            | N2  | place | hamlet   | West Farm | 2 |
+            | N3  | place | hamlet   | East Farm | 3 |
         When importing
         Then place_addressline contains
             | object | address | fromarea |
@@ -17,6 +17,10 @@ Feature: Address computation
         Then place_addressline doesn't contain
             | object | address |
             | N1     | N2      |
+        When searching for "Square"
+        Then results contain
+           | osm_type | osm_id | name              |
+           | N        | 1      | Square, East Farm |
 
     Scenario: given two place nodes, the closer one wins for the address
         Given the grid
@@ -397,3 +401,35 @@ Feature: Address computation
         Then results contain
            | osm_type | osm_id | name                    |
            | N        | 1      | Bolder, Wonderway, Left |
+
+    Scenario: POIs can correct address parts on the fly
+        Given the grid
+            | 1 |   |   |   |  2 |   | 5 |
+            |   |   |   | 9 |    | 8 |   |
+            | 4 |   |   |   |  3 |   | 6 |
+        And the places
+            | osm | class    | type           | admin | name  | geometry    |
+            | R1  | boundary | administrative | 8     | Left  | (1,2,3,4,1) |
+            | R2  | boundary | administrative | 8     | Right | (2,3,6,5,2) |
+        And the places
+            | osm | class   | type    | name      | geometry |
+            | W1  | highway | primary | Wonderway | 2,3      |
+            | N1  | amenity | cafe    | Bolder    | 9        |
+            | N2  | amenity | cafe    | Leftside  | 8        |
+        When importing
+        Then place_addressline contains
+           | object | address | isaddress |
+           | W1     | R1      | False     |
+           | W1     | R2      | True      |
+        And place_addressline doesn't contain
+           | object | address |
+           | N1     | R1      |
+           | N2     | R2      |
+        When searching for "Bolder"
+        Then results contain
+           | osm_type | osm_id | name                    |
+           | N        | 1      | Bolder, Wonderway, Left |
+        When searching for "Leftside"
+        Then results contain
+           | osm_type | osm_id | name                       |
+           | N        | 2      | Leftside, Wonderway, Right |