]> git.openstreetmap.org Git - nominatim.git/commitdiff
Ported functions for the import of special phrases from php to python.
authorAntoJvlt <antonin.jolivat@gmail.com>
Sat, 20 Mar 2021 17:55:08 +0000 (18:55 +0100)
committerAntoJvlt <antonin.jolivat@gmail.com>
Sat, 20 Mar 2021 18:11:50 +0000 (19:11 +0100)
- the command is now --import-special-phrases
- the output is not an sql file anymore, data are directly imported to the database.
- the little part on the documentation (section data import) has been modified.

13 files changed:
CMakeLists.txt
cmake/tool-installed.tmpl
docs/admin/Import.md
lib-sql/functions/normalization.sql
nominatim/cli.py
nominatim/clicmd/__init__.py
nominatim/clicmd/special_phrases.py [new file with mode: 0644]
nominatim/tools/special_phrases.py [new file with mode: 0644]
settings/__init__.py [new file with mode: 0644]
settings/env.defaults
settings/phrase_settings.py [new file with mode: 0644]
test/python/test_cli.py
test/python/test_tools_import_special_phrases.py [new file with mode: 0644]

index 2b4c29765a29cf837049fc33b3f69fe1c2db6cab..45881a4ab47babdf45e7da074c1757cda0153a32 100644 (file)
@@ -114,7 +114,6 @@ if (BUILD_IMPORTER)
        export.php
        query.php
        setup.php
        export.php
        query.php
        setup.php
-       specialphrases.php
        update.php
        warm.php
       )
        update.php
        warm.php
       )
@@ -217,7 +216,7 @@ endif()
 include(GNUInstallDirs)
 set(NOMINATIM_DATADIR ${CMAKE_INSTALL_FULL_DATADIR}/${PROJECT_NAME})
 set(NOMINATIM_LIBDIR ${CMAKE_INSTALL_FULL_LIBDIR}/${PROJECT_NAME})
 include(GNUInstallDirs)
 set(NOMINATIM_DATADIR ${CMAKE_INSTALL_FULL_DATADIR}/${PROJECT_NAME})
 set(NOMINATIM_LIBDIR ${CMAKE_INSTALL_FULL_LIBDIR}/${PROJECT_NAME})
-set(NOMINATIM_CONFIGDIR ${CMAKE_INSTALL_FULL_SYSCONFDIR}/${PROJECT_NAME})
+set(NOMINATIM_CONFIGDIR ${CMAKE_INSTALL_FULL_SYSCONFDIR}/${PROJECT_NAME}/settings)
 
 if (BUILD_IMPORTER)
     configure_file(${PROJECT_SOURCE_DIR}/cmake/tool-installed.tmpl installed.bin)
 
 if (BUILD_IMPORTER)
     configure_file(${PROJECT_SOURCE_DIR}/cmake/tool-installed.tmpl installed.bin)
@@ -258,8 +257,9 @@ if (BUILD_API)
 endif()
 
 install(FILES settings/env.defaults
 endif()
 
 install(FILES settings/env.defaults
+              settings/__init__.py
               settings/address-levels.json
               settings/address-levels.json
-              settings/phrase_settings.php
+              settings/phrase_settings.py
               settings/import-admin.style
               settings/import-street.style
               settings/import-address.style
               settings/import-admin.style
               settings/import-street.style
               settings/import-address.style
index 0b245dbbe74b6dd43e34dfaaa6418aaa559cb570..6128dd2f73e868f7e8545dd0e227e8095f14d3df 100644 (file)
@@ -2,7 +2,9 @@
 import sys
 import os
 
 import sys
 import os
 
-sys.path.insert(1, '@NOMINATIM_LIBDIR@/lib-python')
+sys.path.insert(0, '@NOMINATIM_LIBDIR@/lib-python')
+#Add config directory to the python path for module importation
+sys.path.insert(1, '@NOMINATIM_CONFIGDIR@/..')
 
 os.environ['NOMINATIM_NOMINATIM_TOOL'] = os.path.abspath(__file__)
 
 
 os.environ['NOMINATIM_NOMINATIM_TOOL'] = os.path.abspath(__file__)
 
index ef0da0be5e88135403c0007725e1f49ff7456927..43b11e91e8d278cb7bf8584f077f4a02cf906f39 100644 (file)
@@ -268,10 +268,9 @@ running this function.
 
 If you want to be able to search for places by their type through
 [special key phrases](https://wiki.openstreetmap.org/wiki/Nominatim/Special_Phrases)
 
 If you want to be able to search for places by their type through
 [special key phrases](https://wiki.openstreetmap.org/wiki/Nominatim/Special_Phrases)
-you also need to enable these key phrases like this:
+you also need to import these key phrases like this:
 
 
-    nominatim special-phrases --from-wiki > specialphrases.sql
-    psql -d nominatim -f specialphrases.sql
+    nominatim import-special-phrases --from-wiki
 
 Note that this command downloads the phrases from the wiki link above. You
 need internet access for the step.
 
 Note that this command downloads the phrases from the wiki link above. You
 need internet access for the step.
index 6fcdf55250511077b74366651f062aac45125c55..0300dab401a96323112060f032830496c4151792 100644 (file)
@@ -118,7 +118,7 @@ $$
 LANGUAGE plpgsql;
 
 
 LANGUAGE plpgsql;
 
 
-CREATE OR REPLACE FUNCTION getorcreate_amenity(lookup_word TEXT, normalized_word TEXT,
+CREATE OR REPLACE FUNCTION getorcreate_amenity(lookup_word TEXT,
                                                lookup_class text, lookup_type text)
   RETURNS INTEGER
   AS $$
                                                lookup_class text, lookup_type text)
   RETURNS INTEGER
   AS $$
@@ -128,12 +128,12 @@ DECLARE
 BEGIN
   lookup_token := ' '||trim(lookup_word);
   SELECT min(word_id) FROM word
 BEGIN
   lookup_token := ' '||trim(lookup_word);
   SELECT min(word_id) FROM word
-  WHERE word_token = lookup_token and word = normalized_word
+  WHERE word_token = lookup_token and word = lookup_word
         and class = lookup_class and type = lookup_type
   INTO return_word_id;
   IF return_word_id IS NULL THEN
     return_word_id := nextval('seq_word');
         and class = lookup_class and type = lookup_type
   INTO return_word_id;
   IF return_word_id IS NULL THEN
     return_word_id := nextval('seq_word');
-    INSERT INTO word VALUES (return_word_id, lookup_token, normalized_word,
+    INSERT INTO word VALUES (return_word_id, lookup_token, lookup_word,
                              lookup_class, lookup_type, null, 0);
   END IF;
   RETURN return_word_id;
                              lookup_class, lookup_type, null, 0);
   END IF;
   RETURN return_word_id;
@@ -143,7 +143,6 @@ LANGUAGE plpgsql;
 
 
 CREATE OR REPLACE FUNCTION getorcreate_amenityoperator(lookup_word TEXT,
 
 
 CREATE OR REPLACE FUNCTION getorcreate_amenityoperator(lookup_word TEXT,
-                                                       normalized_word TEXT,
                                                        lookup_class text,
                                                        lookup_type text,
                                                        op text)
                                                        lookup_class text,
                                                        lookup_type text,
                                                        op text)
@@ -155,12 +154,12 @@ DECLARE
 BEGIN
   lookup_token := ' '||trim(lookup_word);
   SELECT min(word_id) FROM word
 BEGIN
   lookup_token := ' '||trim(lookup_word);
   SELECT min(word_id) FROM word
-  WHERE word_token = lookup_token and word = normalized_word
+  WHERE word_token = lookup_token and word = lookup_word
         and class = lookup_class and type = lookup_type and operator = op
   INTO return_word_id;
   IF return_word_id IS NULL THEN
     return_word_id := nextval('seq_word');
         and class = lookup_class and type = lookup_type and operator = op
   INTO return_word_id;
   IF return_word_id IS NULL THEN
     return_word_id := nextval('seq_word');
-    INSERT INTO word VALUES (return_word_id, lookup_token, normalized_word,
+    INSERT INTO word VALUES (return_word_id, lookup_token, lookup_word,
                              lookup_class, lookup_type, null, 0, op);
   END IF;
   RETURN return_word_id;
                              lookup_class, lookup_type, null, 0, op);
   END IF;
   RETURN return_word_id;
index b3d9eee6d735742ef61941379a00ca5477930d68..d6d8e3887cf90ff708720c4415e322b00374d081 100644 (file)
@@ -112,30 +112,6 @@ class CommandlineParser:
 # pylint: disable=C0111
 # Using non-top-level imports to make pyosmium optional for replication only.
 # pylint: disable=E0012,C0415
 # pylint: disable=C0111
 # Using non-top-level imports to make pyosmium optional for replication only.
 # pylint: disable=E0012,C0415
-
-
-class SetupSpecialPhrases:
-    """\
-    Maintain special phrases.
-    """
-
-    @staticmethod
-    def add_args(parser):
-        group = parser.add_argument_group('Input arguments')
-        group.add_argument('--from-wiki', action='store_true',
-                           help='Pull special phrases from the OSM wiki.')
-        group = parser.add_argument_group('Output arguments')
-        group.add_argument('-o', '--output', default='-',
-                           help="""File to write the preprocessed phrases to.
-                                   If omitted, it will be written to stdout.""")
-
-    @staticmethod
-    def run(args):
-        if args.output != '-':
-            raise NotImplementedError('Only output to stdout is currently implemented.')
-        return run_legacy_script('specialphrases.php', '--wiki-import', nominatim_env=args)
-
-
 class UpdateAddData:
     """\
     Add additional data from a file or an online source.
 class UpdateAddData:
     """\
     Add additional data from a file or an online source.
@@ -278,7 +254,7 @@ def nominatim(**kwargs):
     parser.add_subcommand('freeze', clicmd.SetupFreeze)
     parser.add_subcommand('replication', clicmd.UpdateReplication)
 
     parser.add_subcommand('freeze', clicmd.SetupFreeze)
     parser.add_subcommand('replication', clicmd.UpdateReplication)
 
-    parser.add_subcommand('special-phrases', SetupSpecialPhrases)
+    parser.add_subcommand('import-special-phrases', clicmd.ImportSpecialPhrases)
 
     parser.add_subcommand('add-data', UpdateAddData)
     parser.add_subcommand('index', clicmd.UpdateIndex)
 
     parser.add_subcommand('add-data', UpdateAddData)
     parser.add_subcommand('index', clicmd.UpdateIndex)
index 9101e0c08973cc7877849d1b9248e2788cd8457b..ca64f3635031a449f5f9112a0906ccb70de882ce 100644 (file)
@@ -10,3 +10,4 @@ from .refresh import UpdateRefresh
 from .admin import AdminFuncs
 from .freeze import SetupFreeze
 from .transition import AdminTransition
 from .admin import AdminFuncs
 from .freeze import SetupFreeze
 from .transition import AdminTransition
+from .special_phrases import ImportSpecialPhrases
diff --git a/nominatim/clicmd/special_phrases.py b/nominatim/clicmd/special_phrases.py
new file mode 100644 (file)
index 0000000..b7e0f5d
--- /dev/null
@@ -0,0 +1,29 @@
+"""
+    Implementation of the 'import-special-phrases' command.
+"""
+import logging
+from nominatim.tools.special_phrases import import_from_wiki
+from nominatim.db.connection import connect
+
+LOG = logging.getLogger()
+
+# Do not repeat documentation of subcommand classes.
+# pylint: disable=C0111
+
+class ImportSpecialPhrases:
+    """\
+    Import special phrases.
+    """
+    @staticmethod
+    def add_args(parser):
+        group = parser.add_argument_group('Input arguments')
+        group.add_argument('--from-wiki', action='store_true',
+                           help='Import special phrases from the OSM wiki to the database.')
+
+    @staticmethod
+    def run(args):
+        if args.from_wiki:
+            LOG.warning('Special phrases importation starting')
+            with connect(args.config.get_libpq_dsn()) as db_connection:
+                import_from_wiki(args.config, db_connection)
+        return 0
diff --git a/nominatim/tools/special_phrases.py b/nominatim/tools/special_phrases.py
new file mode 100644 (file)
index 0000000..a70d304
--- /dev/null
@@ -0,0 +1,201 @@
+"""
+    Functions to import special phrases into the database.
+"""
+import logging
+import re
+import sys
+from psycopg2.sql import Identifier, Literal, SQL
+from settings.phrase_settings import BLACK_LIST, WHITE_LIST
+from nominatim.tools.exec_utils import get_url
+
+LOG = logging.getLogger()
+
+def import_from_wiki(config, db_connection, languages=None):
+    """
+        Iterate through all specified languages and
+        extract corresponding special phrases from the wiki.
+    """
+    #Compile the match regex to increase performance for the following loop.
+    occurence_pattern = re.compile(
+        r'\| ([^\|]+) \|\| ([^\|]+) \|\| ([^\|]+) \|\| ([^\|]+) \|\| ([\-YN])'
+    )
+    sanity_check_pattern = re.compile(r'^\w+$')
+
+    languages = _get_languages(config) if not languages else languages
+
+    #array for pairs of class/type
+    pairs = dict()
+    for lang in languages:
+        LOG.warning('Import phrases for lang: %s', lang)
+        wiki_page_xml_content = _get_wiki_content(lang)
+        #One match will be of format [label, class, type, operator, plural]
+        matches = occurence_pattern.findall(wiki_page_xml_content)
+
+        for match in matches:
+            phrase_label = match[0].strip()
+            phrase_class = match[1].strip()
+            phrase_type = match[2].strip()
+            phrase_operator = match[3].strip()
+            #hack around a bug where building=yes was imported withq quotes into the wiki
+            phrase_type = re.sub(r'\"|&quot;', '', phrase_type)
+
+            #sanity check, in case somebody added garbage in the wiki
+            _check_sanity(lang, phrase_class, phrase_type, sanity_check_pattern)
+
+            #blacklisting: disallow certain class/type combinations
+            if phrase_class in BLACK_LIST.keys() and phrase_type in BLACK_LIST[phrase_class]:
+                continue
+            #whitelisting: if class is in whitelist, allow only tags in the list
+            if phrase_class in WHITE_LIST.keys() and phrase_type not in WHITE_LIST[phrase_class]:
+                continue
+
+            #add class/type to the pairs dict
+            pairs[f'{phrase_class}|{phrase_type}'] = (phrase_class, phrase_type)
+
+            _process_amenity(
+                db_connection, phrase_label, phrase_class, phrase_type, phrase_operator
+            )
+
+    _create_place_classtype_table_and_indexes(db_connection, config, pairs)
+    db_connection.commit()
+    LOG.warning('Import done.')
+
+
+def _get_languages(config):
+    """
+        Get list of all languages from env config file
+        or default if there is no languages configured.
+        The system will extract special phrases only from all specified languages.
+    """
+    default_languages = [
+        'af', 'ar', 'br', 'ca', 'cs', 'de', 'en', 'es',
+        'et', 'eu', 'fa', 'fi', 'fr', 'gl', 'hr', 'hu',
+        'ia', 'is', 'it', 'ja', 'mk', 'nl', 'no', 'pl',
+        'ps', 'pt', 'ru', 'sk', 'sl', 'sv', 'uk', 'vi']
+    return config.LANGUAGES or default_languages
+
+
+def _get_wiki_content(lang):
+    """
+        Request and return the wiki page's content
+        corresponding to special phrases for a given lang.
+        Requested URL Example :
+            https://wiki.openstreetmap.org/wiki/Special:Export/Nominatim/Special_Phrases/EN
+    """
+    url = 'https://wiki.openstreetmap.org/wiki/Special:Export/Nominatim/Special_Phrases/' + lang.upper() # pylint: disable=line-too-long
+    return get_url(url)
+
+
+def _check_sanity(lang, phrase_class, phrase_type, pattern):
+    """
+        Check sanity of given inputs in case somebody added garbage in the wiki.
+        If a bad class/type is detected the system will exit with an error.
+    """
+    try:
+        if len(pattern.findall(phrase_class)) < 1 or len(pattern.findall(phrase_type)) < 1:
+            sys.exit()
+    except SystemExit:
+        LOG.error("Bad class/type for language %s: %s=%s", lang, phrase_class, phrase_type)
+        raise
+
+
+def _process_amenity(db_connection, phrase_label, phrase_class, phrase_type, phrase_operator):
+    """
+        Add phrase lookup and corresponding class and type to the word table based on the operator.
+    """
+    with db_connection.cursor() as db_cursor:
+        if phrase_operator == 'near':
+            db_cursor.execute("""SELECT getorcreate_amenityoperator(
+                              make_standard_name(%s), %s, %s, 'near')""",
+                              (phrase_label, phrase_class, phrase_type))
+        elif phrase_operator == 'in':
+            db_cursor.execute("""SELECT getorcreate_amenityoperator(
+                              make_standard_name(%s), %s, %s, 'in')""",
+                              (phrase_label, phrase_class, phrase_type))
+        else:
+            db_cursor.execute("""SELECT getorcreate_amenity(
+                              make_standard_name(%s), %s, %s)""",
+                              (phrase_label, phrase_class, phrase_type))
+
+
+def _create_place_classtype_table_and_indexes(db_connection, config, pairs):
+    """
+        Create table place_classtype for each given pair.
+        Also create indexes on place_id and centroid.
+    """
+    LOG.warning('Create tables and indexes...')
+
+    sql_tablespace = config.TABLESPACE_AUX_DATA
+    if sql_tablespace:
+        sql_tablespace = ' TABLESPACE '+sql_tablespace
+
+    with db_connection.cursor() as db_cursor:
+        db_cursor.execute("CREATE INDEX idx_placex_classtype ON placex (class, type)")
+
+    for _, pair in pairs.items():
+        phrase_class = pair[0]
+        phrase_type = pair[1]
+
+        #Table creation
+        _create_place_classtype_table(
+            db_connection, sql_tablespace, phrase_class, phrase_type
+        )
+
+        #Indexes creation
+        _create_place_classtype_indexes(
+            db_connection, sql_tablespace, phrase_class, phrase_type
+        )
+
+        #Grant access on read to the web user.
+        _grant_access_to_webuser(
+            db_connection, config, phrase_class, phrase_type
+        )
+
+    with db_connection.cursor() as db_cursor:
+        db_cursor.execute("DROP INDEX idx_placex_classtype")
+
+
+def _create_place_classtype_table(db_connection, sql_tablespace, phrase_class, phrase_type):
+    """
+        Create table place_classtype of the given phrase_class/phrase_type if doesn't exit.
+    """
+    with db_connection.cursor() as db_cursor:
+        db_cursor.execute(SQL(f"""
+                CREATE TABLE IF NOT EXISTS {{}} {sql_tablespace} 
+                AS SELECT place_id AS place_id,st_centroid(geometry) AS centroid FROM placex 
+                WHERE class = {{}} AND type = {{}}""")
+                          .format(Identifier(f'place_classtype_{phrase_class}_{phrase_type}'),
+                                  Literal(phrase_class), Literal(phrase_type)))
+
+
+def _create_place_classtype_indexes(db_connection, sql_tablespace, phrase_class, phrase_type):
+    """
+        Create indexes on centroid and place_id for the place_classtype table.
+    """
+    #Index on centroid
+    if not db_connection.index_exists(f'idx_place_classtype_{phrase_class}_{phrase_type}_centroid'):
+        with db_connection.cursor() as db_cursor:
+            db_cursor.execute(SQL(f"""
+                    CREATE INDEX {{}} ON {{}} USING GIST (centroid) {sql_tablespace}""")
+                              .format(Identifier(
+                                  f"""idx_place_classtype_{phrase_class}_{phrase_type}_centroid"""),
+                                      Identifier(f'place_classtype_{phrase_class}_{phrase_type}')))
+
+    #Index on place_id
+    if not db_connection.index_exists(f'idx_place_classtype_{phrase_class}_{phrase_type}_place_id'):
+        with db_connection.cursor() as db_cursor:
+            db_cursor.execute(SQL(f"""
+            CREATE INDEX {{}} ON {{}} USING btree(place_id) {sql_tablespace}""")
+                              .format(Identifier(
+                                  f"""idx_place_classtype_{phrase_class}_{phrase_type}_place_id"""),
+                                      Identifier(f'place_classtype_{phrase_class}_{phrase_type}')))
+
+
+def _grant_access_to_webuser(db_connection, config, phrase_class, phrase_type):
+    """
+        Grant access on read to the table place_classtype for the webuser.
+    """
+    with db_connection.cursor() as db_cursor:
+        db_cursor.execute(SQL("""GRANT SELECT ON {} TO {}""")
+                          .format(Identifier(f'place_classtype_{phrase_class}_{phrase_type}'),
+                                  Identifier(config.DATABASE_WEBUSER)))
diff --git a/settings/__init__.py b/settings/__init__.py
new file mode 100644 (file)
index 0000000..b49d97f
--- /dev/null
@@ -0,0 +1,3 @@
+"""
+    Module for settings
+"""
\ No newline at end of file
index 53efb3f79f016ac835bad7944249b3383c80eeaa..78370cf48473a69c612ec1cf0d1520aa39d0cb70 100644 (file)
@@ -77,7 +77,7 @@ NOMINATIM_TIGER_DATA_PATH=
 NOMINATIM_WIKIPEDIA_DATA_PATH=
 
 # Configuration file for special phrase import.
 NOMINATIM_WIKIPEDIA_DATA_PATH=
 
 # Configuration file for special phrase import.
-# When unset, the internal default settings from 'settings/phrase_settings.php'
+# When unset, the internal default settings from 'settings/phrase_settings.py'
 # are used.
 NOMINATIM_PHRASE_CONFIG=
 
 # are used.
 NOMINATIM_PHRASE_CONFIG=
 
diff --git a/settings/phrase_settings.py b/settings/phrase_settings.py
new file mode 100644 (file)
index 0000000..59a4e7c
--- /dev/null
@@ -0,0 +1,26 @@
+"""
+    These settings control the import of special phrases from the wiki.
+"""
+#class/type combinations to exclude
+BLACK_LIST = {
+    'bounday': [
+        'administrative'
+    ],
+    'place': [
+        'house',
+        'houses'
+    ]
+}
+
+#If a class is in the white list then all types will
+#be ignored except the ones given in the list.
+#Also use this list to exclude an entire class from
+#special phrases.
+WHITE_LIST = {
+    'highway': [
+        'bus_stop',
+        'rest_area',
+        'raceway'
+    ],
+    'building': []
+}
index 918d84993d603270638209aadd37eadd920d8f32..0d295f1c9df7b73665fc8be9e7f69421dd3f42be 100644 (file)
@@ -64,7 +64,6 @@ def test_cli_help(capsys):
 
 
 @pytest.mark.parametrize("command,script", [
 
 
 @pytest.mark.parametrize("command,script", [
-                         (('special-phrases',), 'specialphrases'),
                          (('add-data', '--file', 'foo.osm'), 'update'),
                          (('export',), 'export')
                          ])
                          (('add-data', '--file', 'foo.osm'), 'update'),
                          (('export',), 'export')
                          ])
@@ -172,6 +171,12 @@ def test_index_command(mock_func_factory, temp_db_cursor, params, do_bnds, do_ra
     assert bnd_mock.called == do_bnds
     assert rank_mock.called == do_ranks
 
     assert bnd_mock.called == do_bnds
     assert rank_mock.called == do_ranks
 
+def test_special_phrases_command(temp_db, mock_func_factory):
+    func = mock_func_factory(nominatim.clicmd.special_phrases, 'import_from_wiki')
+
+    call_nominatim('import-special-phrases', '--from-wiki')
+
+    assert func.called == 1
 
 @pytest.mark.parametrize("command,func", [
                          ('postcodes', 'update_postcodes'),
 
 @pytest.mark.parametrize("command,func", [
                          ('postcodes', 'update_postcodes'),
diff --git a/test/python/test_tools_import_special_phrases.py b/test/python/test_tools_import_special_phrases.py
new file mode 100644 (file)
index 0000000..058e170
--- /dev/null
@@ -0,0 +1,164 @@
+"""
+    Tests for import special phrases functions
+"""
+import pytest
+from nominatim.tools.special_phrases import _create_place_classtype_indexes, _create_place_classtype_table, _get_wiki_content, _grant_access_to_webuser, _process_amenity
+
+def test_get_wiki_content():
+    assert _get_wiki_content('fr')
+
+def execute_and_verify_add_word(temp_db_conn, phrase_label, phrase_class, phrase_type):
+    _process_amenity(temp_db_conn, phrase_label, phrase_class, phrase_type, '')
+
+    with temp_db_conn.cursor() as temp_db_cursor:
+        temp_db_cursor.execute(f"""
+            SELECT * FROM word 
+            WHERE word_token=' {phrase_label}'
+            AND word='{phrase_label}'
+            AND class='{phrase_class}'
+            AND type='{phrase_type}'""")
+        return temp_db_cursor.fetchone()
+
+def execute_and_verify_add_word_with_operator(temp_db_conn, phrase_label, phrase_class, phrase_type, phrase_operator):
+    _process_amenity(temp_db_conn, phrase_label, phrase_class, phrase_type, phrase_operator)
+
+    with temp_db_conn.cursor() as temp_db_cursor:
+        temp_db_cursor.execute(f"""
+            SELECT * FROM word 
+            WHERE word_token=' {phrase_label}'
+            AND word='{phrase_label}'
+            AND class='{phrase_class}'
+            AND type='{phrase_type}'
+            AND operator='{phrase_operator}'""")
+        return temp_db_cursor.fetchone()
+
+def test_process_amenity_with_near_operator(temp_db_conn, word_table, amenity_operator_funcs):
+    phrase_label = 'label'
+    phrase_class = 'class'
+    phrase_type = 'type'
+
+    assert execute_and_verify_add_word(temp_db_conn, phrase_label, phrase_class, phrase_type)
+    assert execute_and_verify_add_word_with_operator(temp_db_conn, phrase_label, phrase_class, phrase_type, 'near')
+    assert execute_and_verify_add_word_with_operator(temp_db_conn, phrase_label, phrase_class, phrase_type, 'in')
+
+def index_exists(db_connect, index):
+        """ Check that an index with the given name exists in the database.
+        """
+        with db_connect.cursor() as cur:
+            cur.execute("""SELECT tablename FROM pg_indexes
+                           WHERE indexname = %s and schemaname = 'public'""", (index, ))
+            if cur.rowcount == 0:
+                return False
+        return True
+
+def test_create_place_classtype_indexes(temp_db_conn):
+    phrase_class = 'class'
+    phrase_type = 'type'
+    table_name = f'place_classtype_{phrase_class}_{phrase_type}'
+
+    with temp_db_conn.cursor() as temp_db_cursor:
+        temp_db_cursor.execute("CREATE EXTENSION postgis;")
+        temp_db_cursor.execute(f'CREATE TABLE {table_name}(place_id BIGINT, centroid GEOMETRY)')
+
+    _create_place_classtype_indexes(temp_db_conn, '', phrase_class, phrase_type)
+
+    centroid_index_exists = index_exists(temp_db_conn, f'idx_place_classtype_{phrase_class}_{phrase_type}_centroid')
+    place_id_index_exists = index_exists(temp_db_conn, f'idx_place_classtype_{phrase_class}_{phrase_type}_place_id')
+
+    assert centroid_index_exists and place_id_index_exists
+
+def test_create_place_classtype_table(temp_db_conn, placex_table):
+    phrase_class = 'class'
+    phrase_type = 'type'
+    _create_place_classtype_table(temp_db_conn, '', phrase_class, phrase_type)
+
+    with temp_db_conn.cursor() as temp_db_cursor:
+        temp_db_cursor.execute(f"""
+            SELECT *
+            FROM information_schema.tables
+            WHERE table_type='BASE TABLE'
+            AND table_name='place_classtype_{phrase_class}_{phrase_type}'""")
+        result = temp_db_cursor.fetchone()
+    assert result
+
+def test_grant_access_to_web_user(temp_db_conn, def_config):
+    phrase_class = 'class'
+    phrase_type = 'type'
+    table_name = f'place_classtype_{phrase_class}_{phrase_type}'
+
+    with temp_db_conn.cursor() as temp_db_cursor:
+        temp_db_cursor.execute(f'CREATE TABLE {table_name}()')
+
+    _grant_access_to_webuser(temp_db_conn, def_config, phrase_class, phrase_type)
+
+    with temp_db_conn.cursor() as temp_db_cursor:
+        temp_db_cursor.execute(f"""
+                SELECT * FROM information_schema.role_table_grants
+                WHERE table_name='{table_name}' 
+                AND grantee='{def_config.DATABASE_WEBUSER}' 
+                AND privilege_type='SELECT'""")
+        result = temp_db_cursor.fetchone()
+    assert result
+
+@pytest.fixture
+def amenity_operator_funcs(temp_db_cursor):                        
+    temp_db_cursor.execute(f"""
+        CREATE OR REPLACE FUNCTION make_standard_name(name TEXT) RETURNS TEXT
+        AS $$
+        DECLARE
+        o TEXT;
+        BEGIN
+        RETURN name; --Basically return the same name for the tests
+        END;
+        $$
+        LANGUAGE plpgsql IMMUTABLE;
+
+        CREATE SEQUENCE seq_word start 1;
+
+        CREATE OR REPLACE FUNCTION getorcreate_amenity(lookup_word TEXT,
+                                                    lookup_class text, lookup_type text)
+        RETURNS INTEGER
+        AS $$
+        DECLARE
+        lookup_token TEXT;
+        return_word_id INTEGER;
+        BEGIN
+        lookup_token := ' '||trim(lookup_word);
+        SELECT min(word_id) FROM word
+        WHERE word_token = lookup_token and word = lookup_word
+                and class = lookup_class and type = lookup_type
+        INTO return_word_id;
+        IF return_word_id IS NULL THEN
+            return_word_id := nextval('seq_word');
+            INSERT INTO word VALUES (return_word_id, lookup_token, lookup_word,
+                                    lookup_class, lookup_type, null, 0);
+        END IF;
+        RETURN return_word_id;
+        END;
+        $$
+        LANGUAGE plpgsql;
+        
+        CREATE OR REPLACE FUNCTION getorcreate_amenityoperator(lookup_word TEXT,
+                                                        lookup_class text,
+                                                        lookup_type text,
+                                                        op text)
+        RETURNS INTEGER
+        AS $$
+        DECLARE
+        lookup_token TEXT;
+        return_word_id INTEGER;
+        BEGIN
+        lookup_token := ' '||trim(lookup_word);
+        SELECT min(word_id) FROM word
+        WHERE word_token = lookup_token and word = lookup_word
+                and class = lookup_class and type = lookup_type and operator = op
+        INTO return_word_id;
+        IF return_word_id IS NULL THEN
+            return_word_id := nextval('seq_word');
+            INSERT INTO word VALUES (return_word_id, lookup_token, lookup_word,
+                                    lookup_class, lookup_type, null, 0, op);
+        END IF;
+        RETURN return_word_id;
+        END;
+        $$
+        LANGUAGE plpgsql;""")