From: Sarah Hoffmann Date: Sun, 29 Sep 2024 09:44:04 +0000 (+0200) Subject: Merge remote-tracking branch 'upstream/master' X-Git-Url: https://git.openstreetmap.org./nominatim.git/commitdiff_plain/0af8dac3d35a94afe0b6ad775f3226d8d147501d?hp=f289db9bfe21c5a725e751be98e2b824026e2310 Merge remote-tracking branch 'upstream/master' --- diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 1cfaf616..21d506ae 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -131,99 +131,6 @@ jobs: working-directory: Nominatim if: matrix.flavour != 'oldstuff' - legacy-test: - needs: create-archive - runs-on: ubuntu-20.04 - - strategy: - matrix: - postgresql: ["13", "16"] - - steps: - - uses: actions/download-artifact@v4 - with: - name: full-source - - - name: Unpack Nominatim - run: tar xf nominatim-src.tar.bz2 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - - - uses: ./Nominatim/.github/actions/setup-postgresql - with: - postgresql-version: ${{ matrix.postgresql }} - postgis-version: 3 - - - name: Install Postgresql server dev - run: sudo apt-get install postgresql-server-dev-$PGVER - env: - PGVER: ${{ matrix.postgresql }} - - - uses: ./Nominatim/.github/actions/build-nominatim - with: - cmake-args: -DBUILD_MODULE=on - - - name: Install test prerequisites - run: sudo apt-get install -y -qq python3-behave - - - name: BDD tests (legacy tokenizer) - run: | - export PATH=$GITHUB_WORKSPACE/build/osm2pgsql:$PATH - python3 -m behave -DREMOVE_TEMPLATE=1 -DSERVER_MODULE_PATH=$GITHUB_WORKSPACE/build/module -DAPI_ENGINE=php -DTOKENIZER=legacy --format=progress3 - working-directory: Nominatim/test/bdd - - - php-test: - needs: create-archive - runs-on: ubuntu-22.04 - - steps: - - uses: actions/download-artifact@v4 - with: - name: full-source - - - name: Unpack Nominatim - run: tar xf nominatim-src.tar.bz2 - - - uses: ./Nominatim/.github/actions/setup-postgresql - with: - postgresql-version: 15 - postgis-version: 3 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: 8.1 - tools: phpunit:9, phpcs, composer - ini-values: opcache.jit=disable - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: PHP linting - run: phpcs --report-width=120 . - working-directory: Nominatim - - - name: PHP unit tests - run: phpunit ./ - working-directory: Nominatim/test/php - - - uses: ./Nominatim/.github/actions/build-nominatim - with: - flavour: 'ubuntu-22' - - - name: Install test prerequisites - run: sudo apt-get install -y -qq python3-behave - - - name: BDD tests (php) - run: | - export PATH=$GITHUB_WORKSPACE/build/osm2pgsql:$PATH - python3 -m behave -DREMOVE_TEMPLATE=1 -DAPI_ENGINE=php --format=progress3 - working-directory: Nominatim/test/bdd - - install: runs-on: ubuntu-latest needs: create-archive diff --git a/.pylintrc b/.pylintrc index c5915096..e562055d 100644 --- a/.pylintrc +++ b/.pylintrc @@ -13,10 +13,10 @@ ignored-classes=NominatimArgs,closing # 'too-many-ancestors' is triggered already by deriving from UserDict # 'not-context-manager' disabled because it causes false positives once # typed Python is enabled. See also https://github.com/PyCQA/pylint/issues/5273 -disable=too-few-public-methods,duplicate-code,too-many-ancestors,bad-option-value,no-self-use,not-context-manager,use-dict-literal,chained-comparison,attribute-defined-outside-init,too-many-boolean-expressions,contextmanager-generator-missing-cleanup +disable=too-few-public-methods,duplicate-code,too-many-ancestors,bad-option-value,no-self-use,not-context-manager,use-dict-literal,chained-comparison,attribute-defined-outside-init,too-many-boolean-expressions,contextmanager-generator-missing-cleanup,too-many-positional-arguments good-names=i,j,x,y,m,t,fd,db,cc,x1,x2,y1,y2,pt,k,v,nr [DESIGN] -max-returns=7 \ No newline at end of file +max-returns=7 diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b4e3fc9..e6d59520 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") project(nominatim) set(NOMINATIM_VERSION_MAJOR 4) -set(NOMINATIM_VERSION_MINOR 4) +set(NOMINATIM_VERSION_MINOR 5) set(NOMINATIM_VERSION_PATCH 0) set(NOMINATIM_VERSION "${NOMINATIM_VERSION_MAJOR}.${NOMINATIM_VERSION_MINOR}.${NOMINATIM_VERSION_PATCH}") @@ -44,7 +44,6 @@ endif() set(BUILD_IMPORTER on CACHE BOOL "Build everything for importing/updating the database") set(BUILD_API on CACHE BOOL "Build everything for the API server") -set(BUILD_MODULE off CACHE BOOL "Build PostgreSQL module for legacy tokenizer") set(BUILD_TESTS on CACHE BOOL "Build test suite") set(BUILD_OSM2PGSQL on CACHE BOOL "Build osm2pgsql (expert only)") set(INSTALL_MUNIN_PLUGINS on CACHE BOOL "Install Munin plugins for supervising Nominatim") @@ -74,25 +73,6 @@ if (BUILD_IMPORTER OR BUILD_API) find_package(PythonInterp 3.7 REQUIRED) endif() -#----------------------------------------------------------------------------- -# PHP -#----------------------------------------------------------------------------- - -# Setting PHP binary variable as to command line (prevailing) or auto detect - -if (BUILD_API) - if (NOT PHP_BIN) - find_program (PHP_BIN php) - endif() - # sanity check if PHP binary exists - if (NOT EXISTS ${PHP_BIN}) - message(WARNING "PHP binary not found. Only Python frontend can be used.") - set(PHP_BIN "") - else() - message (STATUS "Using PHP binary " ${PHP_BIN}) - endif() -endif() - #----------------------------------------------------------------------------- # import scripts and utilities (importer only) #----------------------------------------------------------------------------- @@ -146,8 +126,6 @@ if (BUILD_TESTS) find_program(PYTHON_BEHAVE behave) find_program(PYLINT NAMES pylint3 pylint) find_program(PYTEST NAMES pytest py.test-3 py.test) - find_program(PHPCS phpcs) - find_program(PHPUNIT phpunit) if (PYTHON_BEHAVE) message(STATUS "Using Python behave binary ${PYTHON_BEHAVE}") @@ -162,24 +140,6 @@ if (BUILD_TESTS) message(WARNING "behave not found. BDD tests disabled." ) endif() - if (PHPUNIT) - message(STATUS "Using phpunit binary ${PHPUNIT}") - add_test(NAME php - COMMAND ${PHPUNIT} ./ - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/test/php) - else() - message(WARNING "phpunit not found. PHP unit tests disabled." ) - endif() - - if (PHPCS) - message(STATUS "Using phpcs binary ${PHPCS}") - add_test(NAME phpcs - COMMAND ${PHPCS} --report-width=120 --colors lib-php - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) - else() - message(WARNING "phpcs not found. PHP linting tests disabled." ) - endif() - if (PYLINT) message(STATUS "Using pylint binary ${PYLINT}") add_test(NAME pylint @@ -199,14 +159,6 @@ if (BUILD_TESTS) endif() endif() -#----------------------------------------------------------------------------- -# Postgres module -#----------------------------------------------------------------------------- - -if (BUILD_MODULE) - add_subdirectory(module) -endif() - #----------------------------------------------------------------------------- # Installation #----------------------------------------------------------------------------- @@ -224,11 +176,7 @@ if (BUILD_IMPORTER) DESTINATION ${CMAKE_INSTALL_BINDIR} RENAME nominatim) - if (EXISTS ${PHP_BIN}) - configure_file(${PROJECT_SOURCE_DIR}/cmake/paths-py.tmpl paths-py.installed) - else() - configure_file(${PROJECT_SOURCE_DIR}/cmake/paths-py-no-php.tmpl paths-py.installed) - endif() + configure_file(${PROJECT_SOURCE_DIR}/cmake/paths-py-no-php.tmpl paths-py.installed) foreach (submodule nominatim_db nominatim_api) install(DIRECTORY src/${submodule} @@ -259,15 +207,6 @@ if (BUILD_OSM2PGSQL) endif() endif() -if (BUILD_MODULE) - install(PROGRAMS ${PROJECT_BINARY_DIR}/module/nominatim.so - DESTINATION ${NOMINATIM_LIBDIR}/module) -endif() - -if (BUILD_API AND EXISTS ${PHP_BIN}) - install(DIRECTORY lib-php DESTINATION ${NOMINATIM_LIBDIR}) -endif() - install(FILES settings/env.defaults settings/address-levels.json settings/phrase-settings.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1df644e7..a78bbfb3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,8 +61,7 @@ pylint3 --extension-pkg-whitelist=osmium nominatim Before submitting a pull request make sure that the tests pass: ``` - cd build - make test + make tests ``` ## Releases @@ -75,7 +74,10 @@ relevant changes are cherry-picked from the master branch. Checklist for releases: -* [ ] increase version in `nominatim/version.py` and CMakeLists.txt +* [ ] increase versions in + * `src/nominatim_api/version.py` + * `src/nominatim_db/version.py` + * CMakeLists.txt * [ ] update `ChangeLog` (copy information from patch releases from release branch) * [ ] complete `docs/admin/Migration.md` * [ ] update EOL dates in `SECURITY.md` @@ -100,3 +102,4 @@ Checklist for releases: * compile and import Nominatim * run `nominatim --version` to confirm correct version * [ ] tag new release and add a release on github.com +* [ ] build pip packages and upload to pypi diff --git a/ChangeLog b/ChangeLog index 2f5d51d5..b7609255 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,34 @@ +4.5.0 + * allow building Nominatim as a pip package + * make osm2pgsql building optional + * switch importer to psycopg3 + * allow output format of web search to be customized in self-installations + * look up potential postcode areas for postcode results + * add word usage statistics for address terms + * implement more light-weight CSV format for wiki importance tables + * rewrite SQL for place search to use window functions + * increase search radius when filtering by postcode + * prefer POI points over POI areas + * reintroduce full terms for address terms in search_name table + * reindex postcodes when their parent is deleted + * indexing: precompute counts of affected rows + * ensure consistent country assignments for overlapping countries + * make Nominatim[Async]API context manager to ensure proper calling of + close() + * make usage of project dir optional for library + * drop interpolations when no parent can be found + * style tweaks to reflect OSM usage (man_made, highway and others) + * deprecation of: bundled osm2pgsql, legacy tokenizer, PHP frontend + * make documentation buildable without CMake + * various fixes and improvements to documentation + +4.4.1 + * fix geocodejson output: admin level output should only print boundaries + * updating: restrict invalidation of child objects on large street features + * restrict valid interpolation house numbers to 0-999999 + * fix import error when SQLAlchemy 1.4 and psycopg3 are installed + * various typo fixes in the documentation + 4.4.0 * add export to SQLite database and SQLite support for the frontend * switch to Python frontend as the default frontend @@ -8,7 +39,7 @@ * fix regression in search with categories where it was confused with near search * partially roll back use of SQLAlchemy lambda statements due to bugs - in SQLAchemy + in SQLAlchemy * fix handling of timezones for timestamps from the database * fix handling of full address searches in connection with a viewbox * fix postcode computation of highway areas diff --git a/SECURITY.md b/SECURITY.md index a14eba13..3ec22cbd 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,10 +9,10 @@ versions. | Version | End of support for security updates | | ------- | ----------------------------------- | +| 4.5.x | 2026-09-12 | | 4.4.x | 2026-03-07 | | 4.3.x | 2025-09-07 | | 4.2.x | 2024-11-24 | -| 4.1.x | 2024-08-05 | ## Reporting a Vulnerability diff --git a/cmake/paths-py-no-php.tmpl b/cmake/paths-py-no-php.tmpl index 36856bf3..a95cb664 100644 --- a/cmake/paths-py-no-php.tmpl +++ b/cmake/paths-py-no-php.tmpl @@ -9,7 +9,6 @@ Path settings for extra data used by Nominatim (installed version). """ from pathlib import Path -PHPLIB_DIR = None SQLLIB_DIR = (Path('@NOMINATIM_LIBDIR@') / 'lib-sql').resolve() DATA_DIR = Path('@NOMINATIM_DATADIR@').resolve() CONFIG_DIR = Path('@NOMINATIM_CONFIGDIR@').resolve() diff --git a/cmake/paths-py.tmpl b/cmake/paths-py.tmpl deleted file mode 100644 index 372a4546..00000000 --- a/cmake/paths-py.tmpl +++ /dev/null @@ -1,15 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# -# This file is part of Nominatim. (https://nominatim.org) -# -# Copyright (C) 2022 by the Nominatim developer community. -# For a full list of authors see the git log. -""" -Path settings for extra data used by Nominatim (installed version). -""" -from pathlib import Path - -PHPLIB_DIR = (Path('@NOMINATIM_LIBDIR@') / 'lib-php').resolve() -SQLLIB_DIR = (Path('@NOMINATIM_LIBDIR@') / 'lib-sql').resolve() -DATA_DIR = Path('@NOMINATIM_DATADIR@').resolve() -CONFIG_DIR = Path('@NOMINATIM_CONFIGDIR@').resolve() diff --git a/docs/admin/Advanced-Installations.md b/docs/admin/Advanced-Installations.md index f8232fb2..de3c5876 100644 --- a/docs/admin/Advanced-Installations.md +++ b/docs/admin/Advanced-Installations.md @@ -131,76 +131,13 @@ script ([Geofabrik](https://download.geofabrik.de)) provides daily updates. ## Using an external PostgreSQL database -You can install Nominatim using a database that runs on a different server when -you have physical access to the file system on the other server. Nominatim -uses a custom normalization library that needs to be made accessible to the -PostgreSQL server. This section explains how to set up the normalization -library. - -!!! note - The external module is only needed when using the legacy tokenizer. - If you have chosen the ICU tokenizer, then you can ignore this section - and follow the standard import documentation. - -### Option 1: Compiling the library on the database server - -The most sure way to get a working library is to compile it on the database -server. From the prerequisites you need at least cmake, gcc and the -PostgreSQL server package. - -Clone or unpack the Nominatim source code, enter the source directory and -create and enter a build directory. - -```sh -cd Nominatim -mkdir build -cd build -``` - -Now configure cmake to only build the PostgreSQL module and build it: - -``` -cmake -DBUILD_IMPORTER=off -DBUILD_API=off -DBUILD_TESTS=off -DBUILD_DOCS=off -DBUILD_OSM2PGSQL=off .. -make -``` - -When done, you find the normalization library in `build/module/nominatim.so`. -Copy it to a place where it is readable and executable by the PostgreSQL server -process. - -### Option 2: Compiling the library on the import machine - -You can also compile the normalization library on the machine from where you -run the import. - -!!! important - You can only do this when the database server and the import machine have - the same architecture and run the same version of Linux. Otherwise there is - no guarantee that the compiled library is compatible with the PostgreSQL - server running on the database server. - -Make sure that the PostgreSQL server package is installed on the machine -**with the same version as on the database server**. You do not need to install -the PostgreSQL server itself. - -Download and compile Nominatim as per standard instructions. Once done, you find -the normalization library in `build/module/nominatim.so`. Copy the file to -the database server at a location where it is readable and executable by the -PostgreSQL server process. - -### Running the import - -On the client side you now need to configure the import to point to the -correct location of the library **on the database server**. Add the following -line to your your `.env` file: - -```php -NOMINATIM_DATABASE_MODULE_PATH="" -``` - -Now change the `NOMINATIM_DATABASE_DSN` to point to your remote server and continue -to follow the [standard instructions for importing](Import.md). +You can install Nominatim using a database that runs on a different server. +Simply point the configuration variable `NOMINATIM_DATABASE_DSN` to the +server and follow the standard import documentation. +The import will be faster, if the import is run directly from the database +machine. You can easily switch to a different machine for the query frontend +after the import. ## Moving the database to another machine @@ -225,20 +162,9 @@ target machine. data updates but the resulting database is only about a third of the size of a full database. -Next install Nominatim on the target machine by following the standard installation -instructions. Again, make sure to use the same version as the source machine. +Next install nominatim-api on the target machine by following the standard +installation instructions. Again, make sure to use the same version as the +source machine. Create a project directory on your destination machine and set up the `.env` -file to match the configuration on the source machine. Finally run - - nominatim refresh --website - -to make sure that the local installation of Nominatim will be used. - -If you are using the legacy tokenizer you might also have to switch to the -PostgreSQL module that was compiled on your target machine. If you get errors -that PostgreSQL cannot find or access `nominatim.so` then rerun - - nominatim refresh --functions - -on the target machine to update the the location of the module. +file to match the configuration on the source machine. That's all. diff --git a/docs/admin/Deployment-PHP.md b/docs/admin/Deployment-PHP.md deleted file mode 100644 index 9416c53e..00000000 --- a/docs/admin/Deployment-PHP.md +++ /dev/null @@ -1,151 +0,0 @@ -# Deploying Nominatim using the PHP frontend - -!!! danger - The PHP frontend is deprecated and will be removed in Nominatim 5.0. - -The Nominatim API is implemented as a PHP application. The `website/` directory -in the project directory contains the configured website. You can serve this -in a production environment with any web server that is capable to run -PHP scripts. - -This section gives a quick overview on how to configure Apache and Nginx to -serve Nominatim. It is not meant as a full system administration guide on how -to run a web service. Please refer to the documentation of -[Apache](https://httpd.apache.org/docs/current/) and -[Nginx](https://nginx.org/en/docs/) -for background information on configuring the services. - -!!! Note - Throughout this page, we assume your Nominatim project directory is - located in `/srv/nominatim-project` and you have installed Nominatim - using the default installation prefix `/usr/local`. If you have put it - somewhere else, you need to adjust the commands and configuration - accordingly. - - We further assume that your web server runs as user `www-data`. Older - versions of CentOS may still use the user name `apache`. You also need - to adapt the instructions in this case. - -## Making the website directory accessible - -You need to make sure that the `website` directory is accessible for the -web server user. You can check that the permissions are correct by accessing -on of the php files as the web server user: - -``` sh -sudo -u www-data head -n 1 /srv/nominatim-project/website/search.php -``` - -If this shows a permission error, then you need to adapt the permissions of -each directory in the path so that it is executable for `www-data`. - -If you have SELinux enabled, further adjustments may be necessary to give the -web server access. At a minimum the following SELinux labelling should be done -for Nominatim: - -``` sh -sudo semanage fcontext -a -t httpd_sys_content_t "/usr/local/nominatim/lib/lib-php(/.*)?" -sudo semanage fcontext -a -t httpd_sys_content_t "/srv/nominatim-project/website(/.*)?" -sudo semanage fcontext -a -t lib_t "/srv/nominatim-project/module/nominatim.so" -sudo restorecon -R -v /usr/local/lib/nominatim -sudo restorecon -R -v /srv/nominatim-project -``` - -## Nominatim with Apache - -### Installing the required packages - -With Apache you can use the PHP module to run Nominatim. - -Under Ubuntu/Debian install them with: - -``` sh -sudo apt install apache2 libapache2-mod-php -``` - -### Configuring Apache - -Make sure your Apache configuration contains the required permissions for the -directory and create an alias: - -``` apache - - Options FollowSymLinks MultiViews - AddType text/html .php - DirectoryIndex search.php - Require all granted - -Alias /nominatim /srv/nominatim-project/website -``` - -After making changes in the apache config you need to restart apache. -The website should now be available on `http://localhost/nominatim`. - -## Nominatim with Nginx - -### Installing the required packages - -Nginx has no built-in PHP interpreter. You need to use php-fpm as a daemon for -serving PHP cgi. - -On Ubuntu/Debian install nginx and php-fpm with: - -``` sh -sudo apt install nginx php-fpm -``` - -### Configure php-fpm and Nginx - -By default php-fpm listens on a network socket. If you want it to listen to a -Unix socket instead, change the pool configuration -(`/etc/php//fpm/pool.d/www.conf`) as follows: - -``` ini -; Replace the tcp listener and add the unix socket -listen = /var/run/php-fpm-nominatim.sock - -; Ensure that the daemon runs as the correct user -listen.owner = www-data -listen.group = www-data -listen.mode = 0666 -``` - -Tell nginx that php files are special and to fastcgi_pass to the php-fpm -unix socket by adding the location definition to the default configuration. - -``` nginx -root /srv/nominatim-project/website; -index search.php; -location / { - try_files $uri $uri/ @php; -} - -location @php { - fastcgi_param SCRIPT_FILENAME "$document_root$uri.php"; - fastcgi_param PATH_TRANSLATED "$document_root$uri.php"; - fastcgi_param QUERY_STRING $args; - fastcgi_pass unix:/var/run/php-fpm-nominatim.sock; - fastcgi_index index.php; - include fastcgi_params; -} - -location ~ [^/]\.php(/|$) { - fastcgi_split_path_info ^(.+?\.php)(/.*)$; - if (!-f $document_root$fastcgi_script_name) { - return 404; - } - fastcgi_pass unix:/var/run/php-fpm-nominatim.sock; - fastcgi_index search.php; - include fastcgi.conf; -} -``` - -Restart the nginx and php-fpm services and the website should now be available -at `http://localhost/`. - -## Nominatim with other webservers - -Users have created instructions for other webservers: - -* [Caddy](https://github.com/osm-search/Nominatim/discussions/2580) - diff --git a/docs/admin/Import.md b/docs/admin/Import.md index 5a365b23..3f248b0e 100644 --- a/docs/admin/Import.md +++ b/docs/admin/Import.md @@ -257,8 +257,8 @@ successfully. nominatim admin --check-database ``` -Now you can try out your installation by executing a simple query on the -command line: +If you have installed the `nominatim-api` package, then you can try out +your installation by executing a simple query on the command line: ``` sh nominatim search --query Berlin @@ -270,10 +270,8 @@ or, when you have a reverse-only installation: nominatim reverse --lat 51 --lon 45 ``` -If you want to run Nominatim as a service, you need to make a choice between -running the modern Python frontend and the legacy PHP frontend. -Make sure you have installed the right packages as per -[Installation](Installation.md#software). +If you want to run Nominatim as a service, make sure you have installed +the right packages as per [Installation](Installation.md#software). #### Testing the Python frontend @@ -291,36 +289,15 @@ or, if you prefer to use Starlette instead of Falcon as webserver, nominatim serve --engine starlette ``` -Go to `http://localhost:8088/status.php` and you should see the message `OK`. -You can also run a search query, e.g. `http://localhost:8088/search.php?q=Berlin` +Go to `http://localhost:8088/status` and you should see the message `OK`. +You can also run a search query, e.g. `http://localhost:8088/search?q=Berlin` or, for reverse-only installations a reverse query, -e.g. `http://localhost:8088/reverse.php?lat=27.1750090510034&lon=78.04209025`. +e.g. `http://localhost:8088/reverse?lat=27.1750090510034&lon=78.04209025`. Do not use this test server in production. To run Nominatim via webservers like Apache or nginx, please continue reading [Deploy the Python frontend](Deployment-Python.md). -#### Testing the PHP frontend - -!!! danger - The PHP fronted is deprecated and will be removed in Nominatim 5.0. - -You can run a small test server with the PHP frontend like this: - -```sh -nominatim serve --engine php -``` - -Go to `http://localhost:8088/status.php` and you should see the message `OK`. -You can also run a search query, e.g. `http://localhost:8088/search.php?q=Berlin` -or, for reverse-only installations a reverse query, -e.g. `http://localhost:8088/reverse.php?lat=27.1750090510034&lon=78.04209025`. - -Do not use this test server in production. -To run Nominatim via webservers like Apache or nginx, please continue reading -[Deploy the PHP frontend](Deployment-PHP.md). - - ## Enabling search by category phrases diff --git a/docs/admin/Installation.md b/docs/admin/Installation.md index e67371bd..78062908 100644 --- a/docs/admin/Installation.md +++ b/docs/admin/Installation.md @@ -72,13 +72,6 @@ For running the Python frontend: * [starlette](https://www.starlette.io/) * [uvicorn](https://www.uvicorn.org/) -For running the legacy PHP frontend (deprecated, will be removed in Nominatim 5.0): - - * [PHP](https://php.net) (7.3+) - * PHP-pgsql - * PHP-intl (bundled with PHP) - - For dependencies for running tests and building documentation, see the [Development section](../develop/Development-Environment.md). @@ -185,18 +178,6 @@ make sudo make install ``` -!!! warning - The default installation no longer compiles the PostgreSQL module that - is needed for the legacy tokenizer from older Nominatim versions. If you - are upgrading an older database or want to run the - [legacy tokenizer](../customize/Tokenizers.md#legacy-tokenizer) for - some other reason, you need to enable the PostgreSQL module via - cmake: `cmake -DBUILD_MODULE=on ../Nominatim`. To compile the module - you need to have the server development headers for PostgreSQL installed. - On Ubuntu/Debian run: `sudo apt install postgresql-server-dev-` - The legacy tokenizer is deprecated and will be removed in Nominatim 5.0 - - Nominatim installs itself into `/usr/local` per default. To choose a different installation directory add `-DCMAKE_INSTALL_PREFIX=` to the cmake command. Make sure that the `bin` directory is available in your path diff --git a/docs/admin/Migration.md b/docs/admin/Migration.md index 75f89141..13e6d7f5 100644 --- a/docs/admin/Migration.md +++ b/docs/admin/Migration.md @@ -1,6 +1,6 @@ # Database Migrations -Nominatim offers automatic migrations since version 3.7. Please follow +Nominatim offers automatic migrations for versions 4.3+. Please follow the following steps: * Stop any updates that are potentially running @@ -17,10 +17,23 @@ Below you find additional migrations and hints about other structural and breaking changes. **Please read them before running the migration.** !!! note - If you are migrating from a version <3.6, then you still have to follow - the manual migration steps up to 3.6. + If you are migrating from a version <4.3, you need to install 4.3 + first and migrate to 4.3 first. Then you can migrate to the current + version. It is strongly recommended to do a reimport instead. -## 4.4.0 -> master +## 4.5.0 -> master + +### PHP frontend removed + +The PHP frontend has been completely removed. Please switch to the Python +frontend. + +Without the PHP code, the `nominatim refresh --website` command is no longer +needed. It currently omits a warning and does otherwise nothing. It will be +removed in later versions of Nominatim. So make sure you remove it from your +scripts. + +## 4.4.0 -> 4.5.0 ### New structure for Python packages diff --git a/docs/api/Details.md b/docs/api/Details.md index c50378c5..b836efd3 100644 --- a/docs/api/Details.md +++ b/docs/api/Details.md @@ -59,13 +59,6 @@ When set, then JSON output will be wrapped in a callback function with the given name. See [JSONP](https://en.wikipedia.org/wiki/JSONP) for more information. -| Parameter | Value | Default | -|-----------| ----- | ------- | -| pretty | 0 or 1 | 0 | - -`[PHP-only]` Add indentation to the output to make it more human-readable. - - ### Output details | Parameter | Value | Default | @@ -95,10 +88,8 @@ members. |-----------| ----- | ------- | | hierarchy | 0 or 1 | 0 | -Include details of places lower in the address hierarchy. - -`[Python-only]` will only return properly parented places. These are address -or POI-like places that reuse the address of their parent street or place. +Include details of POIs and address that depend on the place. Only POIs +that use this place to determine their address will be returned. | Parameter | Value | Default | |-----------| ----- | ------- | @@ -129,7 +120,7 @@ as the ["Accept-Language" HTTP header](https://developer.mozilla.org/en-US/docs/ ##### JSON -[https://nominatim.openstreetmap.org/details.php?osmtype=W&osmid=38210407&format=json](https://nominatim.openstreetmap.org/details.php?osmtype=W&osmid=38210407&format=json) +[https://nominatim.openstreetmap.org/details?osmtype=W&osmid=38210407&format=json](https://nominatim.openstreetmap.org/details?osmtype=W&osmid=38210407&format=json) ```json diff --git a/docs/api/Output.md b/docs/api/Output.md index 029f78bc..75220cf5 100644 --- a/docs/api/Output.md +++ b/docs/api/Output.md @@ -168,7 +168,7 @@ Additional information requested with `addressdetails=1`, `extratags=1` and + more_url="https://nominatim.openstreetmap.org/search?q=london&addressdetails=1&extratags=1&exclude_place_ids=100149&format=xml&accept-language=en-US%2Cen%3Bq%3D0.7%2Cde%3Bq%3D0.3"> /module`) | -| **After Changes:** | run `nominatim refresh --functions` | -| **Comment:** | Legacy tokenizer only | - -Defines the directory in which the PostgreSQL server module `nominatim.so` -is stored. The directory and module must be accessible by the PostgreSQL -server. - -For information on how to use this setting when working with external databases, -see [Advanced Installations](../admin/Advanced-Installations.md). - -The option is only used by the Legacy tokenizer and ignored otherwise. - - #### NOMINATIM_TOKENIZER | Summary | | @@ -115,20 +94,6 @@ on the file format. If a relative path is given, then the file is searched first relative to the project directory and then in the global settings directory. -#### NOMINATIM_MAX_WORD_FREQUENCY - -| Summary | | -| -------------- | --------------------------------------------------- | -| **Description:** | Number of occurrences before a word is considered frequent | -| **Format:** | int | -| **Default:** | 50000 | -| **After Changes:** | cannot be changed after import | -| **Comment:** | Legacy tokenizer only | - -The word frequency count is used by the Legacy tokenizer to automatically -identify _stop words_. Any partial term that occurs more often then what -is defined in this setting, is effectively ignored during search. - #### NOMINATIM_LIMIT_REINDEXING @@ -163,25 +128,6 @@ codes, to restrict import to a subset of languages. Currently only affects the initial import of country names and special phrases. -#### NOMINATIM_TERM_NORMALIZATION - -| Summary | | -| -------------- | --------------------------------------------------- | -| **Description:** | Rules for normalizing terms for comparisons | -| **Format:** | string: semicolon-separated list of ICU rules | -| **Default:** | :: NFD (); [[:Nonspacing Mark:] [:Cf:]] >; :: lower (); [[:Punctuation:][:Space:]]+ > ' '; :: NFC (); | -| **Comment:** | Legacy tokenizer only | - -[Special phrases](Special-Phrases.md) have stricter matching requirements than -normal search terms. They must appear exactly in the query after this term -normalization has been applied. - -Only has an effect on the Legacy tokenizer. For the ICU tokenizer the rules -defined in the -[normalization section](Tokenizers.md#normalization-and-transliteration) -will be used. - - #### NOMINATIM_USE_US_TIGER_DATA | Summary | | @@ -544,38 +490,6 @@ the local languages (in OSM: the name tag without any language suffix) is used. -#### NOMINATIM_SEARCH_BATCH_MODE - -| Summary | | -| -------------- | --------------------------------------------------- | -| **Description:** | Enable a special batch query mode | -| **Format:** | boolean | -| **Default:** | no | -| **After Changes:** | run `nominatim refresh --website` | -| **Comment:** | PHP frontend only | - - -This feature is currently undocumented and potentially broken. - - -#### NOMINATIM_SEARCH_NAME_ONLY_THRESHOLD - -| Summary | | -| -------------- | --------------------------------------------------- | -| **Description:** | Threshold for switching the search index lookup strategy | -| **Format:** | integer | -| **Default:** | 500 | -| **After Changes:** | run `nominatim refresh --website` | -| **Comment:** | PHP frontend only | - -This setting defines the threshold over which a name is no longer considered -as rare. When searching for places with rare names, only the name is used -for place lookups. Otherwise the name and any address information is used. - -This setting only has an effect after `nominatim refresh --word-counts` has -been called to compute the word frequencies. - - #### NOMINATIM_LOOKUP_MAX_COUNT | Summary | | @@ -616,7 +530,6 @@ Setting this parameter to 0 disables polygon output completely. | **Format:** | boolean | | **Default:** | no | | **After Changes:** | run `nominatim refresh --website` | -| **Comment:** | PHP frontend only | Enable to search elements just within countries. @@ -728,7 +641,8 @@ The entries in the log file have the following format: "" Request time is the time when the request was started. The execution time is -given in seconds and corresponds to the time the query took executing in PHP. +given in seconds and includes the entire time the query was queued and executed +in the frontend. type contains the name of the endpoint used. Can be used as the same time as NOMINATIM_LOG_DB. diff --git a/docs/customize/Tokenizers.md b/docs/customize/Tokenizers.md index 49e86a50..30be170e 100644 --- a/docs/customize/Tokenizers.md +++ b/docs/customize/Tokenizers.md @@ -15,53 +15,6 @@ they can be configured. chosen tokenizer is very limited as well. See the comments in each tokenizer section. -## Legacy tokenizer - -!!! danger - The Legacy tokenizer is deprecated and will be removed in Nominatim 5.0. - If you still use a database with the legacy tokenizer, you must reimport - it using the ICU tokenizer below. - -The legacy tokenizer implements the analysis algorithms of older Nominatim -versions. It uses a special Postgresql module to normalize names and queries. -This tokenizer is automatically installed and used when upgrading an older -database. It should not be used for new installations anymore. - -### Compiling the PostgreSQL module - -The tokeinzer needs a special C module for PostgreSQL which is not compiled -by default. If you need the legacy tokenizer, compile Nominatim as follows: - -``` -mkdir build -cd build -cmake -DBUILD_MODULE=on -make -``` - -### Enabling the tokenizer - -To enable the tokenizer add the following line to your project configuration: - -``` -NOMINATIM_TOKENIZER=legacy -``` - -The Postgresql module for the tokenizer is available in the `module` directory -and also installed with the remainder of the software under -`lib/nominatim/module/nominatim.so`. You can specify a custom location for -the module with - -``` -NOMINATIM_DATABASE_MODULE_PATH= -``` - -This is in particular useful when the database runs on a different server. -See [Advanced installations](../admin/Advanced-Installations.md#using-an-external-postgresql-database) for details. - -There are no other configuration options for the legacy tokenizer. All -normalization functions are hard-coded. - ## ICU tokenizer The ICU tokenizer uses the [ICU library](http://site.icu-project.org/) to diff --git a/docs/develop/Development-Environment.md b/docs/develop/Development-Environment.md index fd7820c6..441556ff 100644 --- a/docs/develop/Development-Environment.md +++ b/docs/develop/Development-Environment.md @@ -26,12 +26,9 @@ following packages should get you started: ## Prerequisites for testing and documentation The Nominatim test suite consists of behavioural tests (using behave) and -unit tests (using PHPUnit for PHP code and pytest for Python code). -It has the following additional requirements: +unit tests (using pytest). It has the following additional requirements: * [behave test framework](https://behave.readthedocs.io) >= 1.2.6 -* [phpunit](https://phpunit.de) (9.5 is known to work) -* [PHP CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) * [Pylint](https://pylint.org/) (CI always runs the latest version from pip) * [mypy](http://mypy-lang.org/) (plus typing information for external libs) * [Python Typing Extensions](https://github.com/python/typing_extensions) (for Python < 3.9) @@ -63,7 +60,7 @@ The easiest way, to handle these Python dependencies is to run your development from within a virtual environment. ```sh -sudo apt install libsqlite3-mod-spatialite php-cli +sudo apt install libsqlite3-mod-spatialite ``` To set up the virtual environment with all necessary packages run: @@ -86,28 +83,6 @@ Now enter the virtual environment whenever you want to develop: . ~/nominatim-dev-venv/bin/activate ``` -For installing the PHP development tools, run: - -```sh -sudo apt install php-cgi phpunit php-codesniffer -``` - -If your distribution does not have PHPUnit 7.3+, you can install it (as well -as CodeSniffer) via composer: - -``` -sudo apt-get install composer -composer global require "squizlabs/php_codesniffer=*" -composer global require "phpunit/phpunit=8.*" -``` - -The binaries are found in `.config/composer/vendor/bin`. You need to add this -to your PATH: - -``` -echo 'export PATH=~/.config/composer/vendor/bin:$PATH' > ~/.profile -``` - ### Running Nominatim during development The source code for Nominatim can be found in the `src` directory and can diff --git a/docs/develop/Testing.md b/docs/develop/Testing.md index c220f4e4..12673d40 100644 --- a/docs/develop/Testing.md +++ b/docs/develop/Testing.md @@ -8,7 +8,7 @@ the tests, see the [Development setup chapter](Development-Environment.md). There are two kind of tests in this test suite. There are functional tests which test the API interface using a BDD test framework and there are unit -tests for specific PHP functions. +tests for the Python code. This test directory is structured as follows: @@ -20,28 +20,11 @@ This test directory is structured as follows: | +- db Tests for internal data processing on import and update | +- api Tests for API endpoints (search, reverse, etc.) | - +- php PHP unit tests +- python Python unit tests +- testdb Base data for generating API test database +- testdata Additional test data used by unit tests ``` -## PHP Unit Tests (`test/php`) - -Unit tests for PHP code can be found in the `php/` directory. They test selected -PHP functions. Very low coverage. - -To execute the test suite run - - cd test/php - UNIT_TEST_DSN='pgsql:dbname=nominatim_unit_tests' phpunit ../ - -It will read phpunit.xml which points to the library, test path, bootstrap -strip and sets other parameters. - -It will use (and destroy) a local database 'nominatim_unit_tests'. You can set -a different connection string with e.g. UNIT_TEST_DSN='pgsql:dbname=foo_unit_tests'. - ## Python Unit Tests (`test/python`) Unit tests for Python code can be found in the `python/` directory. The goal is @@ -89,8 +72,6 @@ The tests can be configured with a set of environment variables (`behave -D key= * `DB_PORT` - (optional) port of database on host * `DB_USER` - (optional) username of database login * `DB_PASS` - (optional) password for database login - * `SERVER_MODULE_PATH` - (optional) path on the Postgres server to Nominatim - module shared library file (only needed for legacy tokenizer) * `REMOVE_TEMPLATE` - if true, the template and API database will not be reused during the next run. Reusing the base templates speeds up tests considerably but might lead to outdated errors @@ -118,7 +99,7 @@ and compromises the following data: * extract of Autauga country, Alabama, US (for tests against Tiger data) * additional data from `test/testdb/additional_api_test.data.osm` -API tests should only be testing the functionality of the website PHP code. +API tests should only be testing the functionality of the website frontend code. Most tests should be formulated as BDD DB creation tests (see below) instead. ### DB Creation Tests (`test/bdd/db`) diff --git a/docs/develop/Tokenizers.md b/docs/develop/Tokenizers.md index 03988ce0..f4a55adc 100644 --- a/docs/develop/Tokenizers.md +++ b/docs/develop/Tokenizers.md @@ -91,14 +91,9 @@ for a custom tokenizer implementation. ### Directory Structure -Nominatim expects two files for a tokenizer: - -* `nominatim/tokenizer/_tokenizer.py` containing the Python part of the - implementation -* `lib-php/tokenizer/_tokenizer.php` with the PHP part of the - implementation - -where `` is a unique name for the tokenizer consisting of only lower-case +Nominatim expects a single file `src/nominatim_db/tokenizer/_tokenizer.py` +containing the Python part of the implementation. +`` is a unique name for the tokenizer consisting of only lower-case letters, digits and underscore. A tokenizer also needs to install some SQL functions. By convention, these should be placed in `lib-sql/tokenizer`. @@ -282,73 +277,3 @@ permanently. The indexer calls this function when all processing is done and replaces the content of the `token_info` column with the returned value before the trigger stores the information in the database. May return NULL if no information should be stored permanently. - -### PHP Tokenizer class - -The PHP tokenizer class is instantiated once per request and responsible for -analyzing the incoming query. Multiple requests may be in flight in -parallel. - -The class is expected to be found under the -name of `\Nominatim\Tokenizer`. To find the class the PHP code includes the file -`tokenizer/tokenizer.php` in the project directory. This file must be created -when the tokenizer is first set up on import. The file should initialize any -configuration variables by setting PHP constants and then require the file -with the actual implementation of the tokenizer. - -The tokenizer class must implement the following functions: - -```php -public function __construct(object &$oDB) -``` - -The constructor of the class receives a database connection that can be used -to query persistent data in the database. - -```php -public function checkStatus() -``` - -Check that the tokenizer can access its persistent data structures. If there -is an issue, throw an `\Exception`. - -```php -public function normalizeString(string $sTerm) : string -``` - -Normalize string to a form to be used for comparisons when reordering results. -Nominatim reweighs results how well the final display string matches the actual -query. Before comparing result and query, names and query are normalised against -this function. The tokenizer can thus remove all properties that should not be -taken into account for reweighing, e.g. special characters or case. - -```php -public function tokensForSpecialTerm(string $sTerm) : array -``` - -Return the list of special term tokens that match the given term. - -```php -public function extractTokensFromPhrases(array &$aPhrases) : TokenList -``` - -Parse the given phrases, splitting them into word lists and retrieve the -matching tokens. - -The phrase array may take on two forms. In unstructured searches (using `q=` -parameter) the search query is split at the commas and the elements are -put into a sorted list. For structured searches the phrase array is an -associative array where the key designates the type of the term (street, city, -county etc.) The tokenizer may ignore the phrase type at this stage in parsing. -Matching phrase type and appropriate search token type will be done later -when the SearchDescription is built. - -For each phrase in the list of phrases, the function must analyse the phrase -string and then call `setWordSets()` to communicate the result of the analysis. -A word set is a list of strings, where each string refers to a search token. -A phrase may have multiple interpretations. Therefore a list of word sets is -usually attached to the phrase. The search tokens themselves are returned -by the function in an associative array, where the key corresponds to the -strings given in the word sets. The value is a list of search tokens. Thus -a single string in the list of word sets may refer to multiple search tokens. - diff --git a/docs/develop/overview.md b/docs/develop/overview.md index b5625a88..aedce990 100644 --- a/docs/develop/overview.md +++ b/docs/develop/overview.md @@ -20,5 +20,5 @@ and can be found in the files in the `sql/functions/` directory. The __search frontend__ implements the actual API. It takes search and reverse geocoding queries from the user, looks up the data and -returns the results in the requested format. This part is written in PHP -and can be found in the `lib/` and `website/` directories. +returns the results in the requested format. This part is located in the +`nominatim-api` package. The source code can be found in `src/nominatim_api`. diff --git a/docs/extra.css b/docs/extra.css index 155fa1aa..1decc478 100644 --- a/docs/extra.css +++ b/docs/extra.css @@ -34,3 +34,8 @@ th { .md-footer__inner { display: none; } + +.headerlink { + filter: grayscale(100%); + font-size: 80%; +} diff --git a/docs/library/Configuration.md b/docs/library/Configuration.md index e13470e9..713d1c53 100644 --- a/docs/library/Configuration.md +++ b/docs/library/Configuration.md @@ -1,11 +1,13 @@ # Configuration When using Nominatim through the library, it can be configured in exactly -the same way as when running as a service. This means that you should have -created a [project directory](../admin/Import.md#creating-the-project-directory) -which contains all files belonging to the Nominatim instance. It can also contain -an `.env` file with configuration options. Setting configuration parameters -via environment variables works as well. +the same way as when running as a service. You may instantiate the library +against the [project directory](../admin/Import.md#creating-the-project-directory) +of your Nominatim installation. It contains all files belonging to the +Nominatim instance. This may include an `.env` file with configuration options. +Setting configuration parameters via environment variables works as well. +Alternatively to using the operating system's environment, a set of +configuration parameters may also be passed to the Nomiantim API object. Configuration options are resolved in the following order: diff --git a/docs/library/Getting-Started.md b/docs/library/Getting-Started.md index 1f5b2baa..9f81724a 100644 --- a/docs/library/Getting-Started.md +++ b/docs/library/Getting-Started.md @@ -1,16 +1,21 @@ # Getting Started -The Nominatim search frontend can directly be used as a Python library in -scripts and applications. When you have imported your own Nominatim database, -then it is no longer necessary to run a full web service for it and access -the database through http requests. There are -also less constraints on the kinds of data that can be accessed. The library -allows to get access to more detailed information about the objects saved -in the database. - -!!! danger - The library interface is currently in an experimental stage. There might - be some smaller adjustments to the public interface until the next version. +The Nominatim search frontend is implemented as a Python library and can as +such directly be used in Python scripts and applications. You don't need to +set up a web frontend and access it through HTTP calls. The library gives +direct access to the Nominatim database through similar search functions as +offered by the web API. In addition, it will give you a more complete and +detailed view on the search objects stored in the database. + +!!! warning + + The Nominatim library is used for accessing a local Nominatim database. + It is not meant to be used against web services of Nominatim like the + one on https://nominatim.openstreetmap.org. If you need a Python library + to access these web services, have a look at + [GeoPy](https://geopy.readthedocs.io). Don't forget to consult the + usage policy of the service you want to use before accessing such + a web service. ## Installation @@ -19,13 +24,17 @@ Follow the [installation](../admin/Installation.md) and [import](../admin/Import.md) instructions to set up your database. The Nominatim frontend library is contained in the Python package `nominatim-api`. +You can install the latest released version directly from pip: + + pip install nominatim-api + To install the package from the source tree directly, run: pip install packaging/nominatim-api Usually you would want to run this in a virtual environment. -### A simple search example +## A simple search example To query the Nominatim database you need to first set up a connection. This is done by creating an Nominatim API object. This object exposes all the @@ -36,15 +45,13 @@ This code snippet implements a simple search for the town of 'Brugge': !!! example === "NominatimAPIAsync" ``` python - from pathlib import Path import asyncio import nominatim_api as napi async def search(query): - api = napi.NominatimAPIAsync(Path('.')) - - return await api.search(query) + async with napi.NominatimAPIAsync() as api: + return await api.search(query) results = asyncio.run(search('Brugge')) if not results: @@ -55,13 +62,10 @@ This code snippet implements a simple search for the town of 'Brugge': === "NominatimAPI" ``` python - from pathlib import Path - import nominatim_api as napi - api = napi.NominatimAPI(Path('.')) - - results = api.search('Brugge') + with napi.NominatimAPI() as api: + results = api.search('Brugge') if not results: print('Cannot find Brugge') @@ -84,7 +88,7 @@ implementations. The documentation itself will usually refer only to available only for the synchronous or asynchronous version, this will be explicitly mentioned. -### Defining which database to use +## Defining which database to use The [Configuration](../admin/Import.md#configuration-setup-in-env) section explains how Nominatim is configured using the @@ -93,25 +97,65 @@ The same configuration mechanism is used with the Nominatim API library. You should therefore be sure you are familiar with the section. -The constructor of the 'Nominatim API class' takes one mandatory parameter: -the path to the [project directory](../admin/Import.md#creating-the-project-directory). -You should have set up this directory as part of the Nominatim import. -Any configuration found in the `.env` file in this directory will automatically -used. +There are three different ways, how configuration options can be set for +a 'Nominatim API class'. When you have set up your Nominatim database, you +have normally created a [project directory](../admin/Import.md#creating-the-project-directory) +which stores the various configuration and customization files that Nominatim +needs. You may pass the location of the project directory to your +'Nominatim API class' constructor and it will read the .env file in the +directory and set the configuration accordingly. Here is the simple search +example, using the configuration from a pre-defined project directory in +`/srv/nominatim-project`: + +!!! example + === "NominatimAPIAsync" + ``` python + import asyncio + + import nominatim_api as napi + + async def search(query): + async with napi.NominatimAPIAsync('/srv/nominatim-project') as api: + return await api.search(query) + + results = asyncio.run(search('Brugge')) + if not results: + print('Cannot find Brugge') + else: + print(f'Found a place at {results[0].centroid.x},{results[0].centroid.y}') + ``` + + === "NominatimAPI" + ``` python + import nominatim_api as napi + + with napi.NominatimAPI('/srv/nominatim-project') as api: + results = api.search('Brugge') + + if not results: + print('Cannot find Brugge') + else: + print(f'Found a place at {results[0].centroid.x},{results[0].centroid.y}') + ``` + You may also configure Nominatim by setting environment variables. -Normally, Nominatim will check the operating system environment. This can be -overwritten by giving the constructor a dictionary of configuration parameters. +Normally Nominatim will check the operating system environment. Lets +say you want to look up 'Brugge' in the special database named 'belgium' instead of the +standard 'nominatim' database. You can run the example script above like this: + +``` +NOMINATIM_DATABASE_DSN=pgsql:dbname=belgium python3 example.py +``` -Let us look up 'Brugge' in the special database named 'belgium' instead of the -standard 'nominatim' database: +The third option to configure the library is to hand in the configuration +parameters into the 'Nominatim API class'. Changing the database would look +like this: !!! example === "NominatimAPIAsync" ``` python - from pathlib import Path import asyncio - import nominatim_api as napi config_params = { @@ -119,50 +163,54 @@ standard 'nominatim' database: } async def search(query): - api = napi.NominatimAPIAsync(Path('.'), environ=config_params) - - return await api.search(query) + async with napi.NominatimAPIAsync(environ=config_params) as api: + return await api.search(query) results = asyncio.run(search('Brugge')) ``` === "NominatimAPI" ``` python - from pathlib import Path - import nominatim_api as napi config_params = { 'NOMINATIM_DATABASE_DSN': 'pgsql:dbname=belgium' } - api = napi.NominatimAPI(Path('.'), environ=config_params) - - results = api.search('Brugge') + with napi.NominatimAPI(environ=config_params) as api: + results = api.search('Brugge') ``` -### Presenting results to humans +When the `environ` parameter is given, then only configuration variables +from this dictionary will be used. The operating system's environment +variables will be ignored. -All search functions return the raw results from the database. There is no -full human-readable label. To create such a label, you need two things: +## Presenting results to humans + +All search functions return full result objects from the database. Such a +result object contains lots of details: names, address information, OSM tags etc. +This gives you lots of flexibility what to do with the results. + +One of the most common things to get is some kind of human-readable label +that describes the result in a compact form. Usually this would be the name +of the object and some parts of the address to explain where in the world +it is. To create such a label, you need two things: * the address details of the place -* adapt the result to the language you wish to use for display +* all names for the label adapted to the language you wish to use for display Again searching for 'Brugge', this time with a nicely formatted result: !!! example === "NominatimAPIAsync" ``` python - from pathlib import Path import asyncio import nominatim_api as napi async def search(query): - api = napi.NominatimAPIAsync(Path('.')) - - return await api.search(query, address_details=True) + async with napi.NominatimAPIAsync() as api: + return await api.search(query, address_details=True) results = asyncio.run(search('Brugge')) @@ -174,13 +222,10 @@ Again searching for 'Brugge', this time with a nicely formatted result: === "NominatimAPI" ``` python - from pathlib import Path - import nominatim_api as napi - api = napi.NominatimAPI(Path('.')) - - results = api.search('Brugge', address_details=True) + with napi.NominatimAPI() as api: + results = api.search('Brugge', address_details=True) locale = napi.Locales(['fr', 'en']) for i, result in enumerate(results): @@ -236,7 +281,7 @@ Bruges, Flandre-Occidentale, Flandre, Belgique This is a fairly simple way to create a human-readable description. The place information in `address_rows` contains further information about each -place. For example, which OSM `adlin_level` was used, what category the place +place. For example, which OSM `admin_level` was used, what category the place belongs to or what rank Nominatim has assigned. Use this to adapt the output to local address formats. diff --git a/docs/library/Low-Level-DB-Access.md b/docs/library/Low-Level-DB-Access.md index 84a40b9b..96699061 100644 --- a/docs/library/Low-Level-DB-Access.md +++ b/docs/library/Low-Level-DB-Access.md @@ -24,12 +24,11 @@ the placex table: ``` import asyncio -from pathlib import Path import sqlalchemy as sa from nominatim_api import NominatimAPIAsync async def print_table_size(): - api = NominatimAPIAsync(Path('.')) + api = NominatimAPIAsync() async with api.begin() as conn: cnt = await conn.scalar(sa.select(sa.func.count()).select_from(conn.t.placex)) diff --git a/lib-php/AddressDetails.php b/lib-php/AddressDetails.php deleted file mode 100644 index cfdd0416..00000000 --- a/lib-php/AddressDetails.php +++ /dev/null @@ -1,191 +0,0 @@ -iPlaceID = $iPlaceID; - - if (is_array($mLangPref)) { - $mLangPref = $oDB->getArraySQL($oDB->getDBQuotedList($mLangPref)); - } - - if (!isset($sHousenumber)) { - $sHousenumber = -1; - } - - $sSQL = 'SELECT *,'; - $sSQL .= ' get_name_by_language(name,'.$mLangPref.') as localname'; - $sSQL .= ' FROM get_addressdata('.$iPlaceID.','.$sHousenumber.')'; - $sSQL .= ' ORDER BY rank_address DESC, isaddress DESC'; - - $this->aAddressLines = $oDB->getAll($sSQL); - } - - private static function isAddress($aLine) - { - return $aLine['isaddress'] || $aLine['type'] == 'country_code'; - } - - public function getAddressDetails($bAll = false) - { - if ($bAll) { - return $this->aAddressLines; - } - - return array_filter($this->aAddressLines, array(__CLASS__, 'isAddress')); - } - - public function getLocaleAddress() - { - $aParts = array(); - $sPrevResult = ''; - - foreach ($this->aAddressLines as $aLine) { - if ($aLine['isaddress'] && $sPrevResult != $aLine['localname']) { - $sPrevResult = $aLine['localname']; - $aParts[] = $sPrevResult; - } - } - - return join(', ', $aParts); - } - - public function getAddressNames() - { - $aAddress = array(); - - foreach ($this->aAddressLines as $aLine) { - if (!self::isAddress($aLine)) { - continue; - } - - $sTypeLabel = ClassTypes\getLabelTag($aLine); - - $sName = null; - if (isset($aLine['localname']) && $aLine['localname']!=='') { - $sName = $aLine['localname']; - } elseif (isset($aLine['housenumber']) && $aLine['housenumber']!=='') { - $sName = $aLine['housenumber']; - } - - if (isset($sName) - && (!isset($aAddress[$sTypeLabel]) - || $aLine['class'] == 'place') - ) { - $aAddress[$sTypeLabel] = $sName; - - if (!empty($aLine['name'])) { - $this->addSubdivisionCode($aAddress, $aLine['admin_level'], $aLine['name']); - } - } - } - - return $aAddress; - } - - /** - * Annotates the given json with geocodejson address information fields. - * - * @param array $aJson Json hash to add the fields to. - * - * Geocodejson has the following fields: - * street, locality, postcode, city, district, - * county, state, country - * - * Postcode and housenumber are added by type, district is not used. - * All other fields are set according to address rank. - */ - public function addGeocodeJsonAddressParts(&$aJson) - { - foreach (array_reverse($this->aAddressLines) as $aLine) { - if (!$aLine['isaddress']) { - continue; - } - - if (!isset($aLine['localname']) || $aLine['localname'] == '') { - continue; - } - - if ($aLine['type'] == 'postcode' || $aLine['type'] == 'postal_code') { - $aJson['postcode'] = $aLine['localname']; - continue; - } - - if ($aLine['type'] == 'house_number') { - $aJson['housenumber'] = $aLine['localname']; - continue; - } - - if ($this->iPlaceID == $aLine['place_id']) { - continue; - } - - $iRank = (int)$aLine['rank_address']; - - if ($iRank > 25 && $iRank < 28) { - $aJson['street'] = $aLine['localname']; - } elseif ($iRank >= 22 && $iRank <= 25) { - $aJson['locality'] = $aLine['localname']; - } elseif ($iRank >= 17 && $iRank <= 21) { - $aJson['district'] = $aLine['localname']; - } elseif ($iRank >= 13 && $iRank <= 16) { - $aJson['city'] = $aLine['localname']; - } elseif ($iRank >= 10 && $iRank <= 12) { - $aJson['county'] = $aLine['localname']; - } elseif ($iRank >= 5 && $iRank <= 9) { - $aJson['state'] = $aLine['localname']; - } elseif ($iRank == 4) { - $aJson['country'] = $aLine['localname']; - } - } - } - - public function getAdminLevels() - { - $aAddress = array(); - foreach (array_reverse($this->aAddressLines) as $aLine) { - if (self::isAddress($aLine) - && isset($aLine['admin_level']) - && $aLine['admin_level'] < 15 - && !isset($aAddress['level'.$aLine['admin_level']]) - ) { - $aAddress['level'.$aLine['admin_level']] = $aLine['localname']; - } - } - return $aAddress; - } - - public function debugInfo() - { - return $this->aAddressLines; - } - - private function addSubdivisionCode(&$aAddress, $iAdminLevel, $nameDetails) - { - if (is_string($nameDetails)) { - $nameDetails = json_decode('{' . str_replace('"=>"', '":"', $nameDetails) . '}', true); - } - if (!empty($nameDetails['ISO3166-2'])) { - $aAddress["ISO3166-2-lvl$iAdminLevel"] = $nameDetails['ISO3166-2']; - } - } -} diff --git a/lib-php/ClassTypes.php b/lib-php/ClassTypes.php deleted file mode 100644 index 0561f482..00000000 --- a/lib-php/ClassTypes.php +++ /dev/null @@ -1,576 +0,0 @@ - array ( - 1 => 'Continent', - 2 => 'Country', - 3 => 'Region', - 4 => 'State', - 5 => 'State District', - 6 => 'County', - 7 => 'Municipality', - 8 => 'City', - 9 => 'City District', - 10 => 'Suburb', - 11 => 'Neighbourhood', - 12 => 'City Block' - ), - 'no' => array ( - 3 => 'State', - 4 => 'County' - ), - 'se' => array ( - 3 => 'State', - 4 => 'County' - ) - ); - - if (isset($aBoundaryList[$sCountry]) - && isset($aBoundaryList[$sCountry][$iAdminLevel]) - ) { - return $aBoundaryList[$sCountry][$iAdminLevel]; - } - - return $aBoundaryList['default'][$iAdminLevel] ?? $sFallback; -} - -/** - * Return an estimated radius of how far the object node extends. - * - * @param array[] $aPlace Information about the place. This must be a node - * feature. - * - * @return float The radius around the feature in degrees. - */ -function getDefRadius($aPlace) -{ - $aSpecialRadius = array( - 'place:continent' => 25, - 'place:country' => 7, - 'place:state' => 2.6, - 'place:province' => 2.6, - 'place:region' => 1.0, - 'place:county' => 0.7, - 'place:city' => 0.16, - 'place:municipality' => 0.16, - 'place:island' => 0.32, - 'place:postcode' => 0.16, - 'place:town' => 0.04, - 'place:village' => 0.02, - 'place:hamlet' => 0.02, - 'place:district' => 0.02, - 'place:borough' => 0.02, - 'place:suburb' => 0.02, - 'place:locality' => 0.01, - 'place:neighbourhood'=> 0.01, - 'place:quarter' => 0.01, - 'place:city_block' => 0.01, - 'landuse:farm' => 0.01, - 'place:farm' => 0.01, - 'place:airport' => 0.015, - 'aeroway:aerodrome' => 0.015, - 'railway:station' => 0.005 - ); - - $sClassPlace = $aPlace['class'].':'.$aPlace['type']; - - return $aSpecialRadius[$sClassPlace] ?? 0.00005; -} - -/** - * Get the icon to use with the given object. - */ -function getIcon($aPlace) -{ - $aIcons = array( - 'boundary:administrative' => 'poi_boundary_administrative', - 'place:city' => 'poi_place_city', - 'place:town' => 'poi_place_town', - 'place:village' => 'poi_place_village', - 'place:hamlet' => 'poi_place_village', - 'place:suburb' => 'poi_place_village', - 'place:locality' => 'poi_place_village', - 'place:airport' => 'transport_airport2', - 'aeroway:aerodrome' => 'transport_airport2', - 'railway:station' => 'transport_train_station2', - 'amenity:place_of_worship' => 'place_of_worship_unknown3', - 'amenity:pub' => 'food_pub', - 'amenity:bar' => 'food_bar', - 'amenity:university' => 'education_university', - 'tourism:museum' => 'tourist_museum', - 'amenity:arts_centre' => 'tourist_art_gallery2', - 'tourism:zoo' => 'tourist_zoo', - 'tourism:theme_park' => 'poi_point_of_interest', - 'tourism:attraction' => 'poi_point_of_interest', - 'leisure:golf_course' => 'sport_golf', - 'historic:castle' => 'tourist_castle', - 'amenity:hospital' => 'health_hospital', - 'amenity:school' => 'education_school', - 'amenity:theatre' => 'tourist_theatre', - 'amenity:library' => 'amenity_library', - 'amenity:fire_station' => 'amenity_firestation3', - 'amenity:police' => 'amenity_police2', - 'amenity:bank' => 'money_bank2', - 'amenity:post_office' => 'amenity_post_office', - 'tourism:hotel' => 'accommodation_hotel2', - 'amenity:cinema' => 'tourist_cinema', - 'tourism:artwork' => 'tourist_art_gallery2', - 'historic:archaeological_site' => 'tourist_archaeological2', - 'amenity:doctors' => 'health_doctors', - 'leisure:sports_centre' => 'sport_leisure_centre', - 'leisure:swimming_pool' => 'sport_swimming_outdoor', - 'shop:supermarket' => 'shopping_supermarket', - 'shop:convenience' => 'shopping_convenience', - 'amenity:restaurant' => 'food_restaurant', - 'amenity:fast_food' => 'food_fastfood', - 'amenity:cafe' => 'food_cafe', - 'tourism:guest_house' => 'accommodation_bed_and_breakfast', - 'amenity:pharmacy' => 'health_pharmacy_dispensing', - 'amenity:fuel' => 'transport_fuel', - 'natural:peak' => 'poi_peak', - 'natural:wood' => 'landuse_coniferous_and_deciduous', - 'shop:bicycle' => 'shopping_bicycle', - 'shop:clothes' => 'shopping_clothes', - 'shop:hairdresser' => 'shopping_hairdresser', - 'shop:doityourself' => 'shopping_diy', - 'shop:estate_agent' => 'shopping_estateagent2', - 'shop:car' => 'shopping_car', - 'shop:garden_centre' => 'shopping_garden_centre', - 'shop:car_repair' => 'shopping_car_repair', - 'shop:bakery' => 'shopping_bakery', - 'shop:butcher' => 'shopping_butcher', - 'shop:apparel' => 'shopping_clothes', - 'shop:laundry' => 'shopping_laundrette', - 'shop:beverages' => 'shopping_alcohol', - 'shop:alcohol' => 'shopping_alcohol', - 'shop:optician' => 'health_opticians', - 'shop:chemist' => 'health_pharmacy', - 'shop:gallery' => 'tourist_art_gallery2', - 'shop:jewelry' => 'shopping_jewelry', - 'tourism:information' => 'amenity_information', - 'historic:ruins' => 'tourist_ruin', - 'amenity:college' => 'education_school', - 'historic:monument' => 'tourist_monument', - 'historic:memorial' => 'tourist_monument', - 'historic:mine' => 'poi_mine', - 'tourism:caravan_site' => 'accommodation_caravan_park', - 'amenity:bus_station' => 'transport_bus_station', - 'amenity:atm' => 'money_atm2', - 'tourism:viewpoint' => 'tourist_view_point', - 'tourism:guesthouse' => 'accommodation_bed_and_breakfast', - 'railway:tram' => 'transport_tram_stop', - 'amenity:courthouse' => 'amenity_court', - 'amenity:recycling' => 'amenity_recycling', - 'amenity:dentist' => 'health_dentist', - 'natural:beach' => 'tourist_beach', - 'railway:tram_stop' => 'transport_tram_stop', - 'amenity:prison' => 'amenity_prison', - 'highway:bus_stop' => 'transport_bus_stop2' - ); - - $sClassPlace = $aPlace['class'].':'.$aPlace['type']; - - return $aIcons[$sClassPlace] ?? null; -} - -/** - * Get an icon for the given object with its full URL. - */ -function getIconFile($aPlace) -{ - if (CONST_MapIcon_URL === false) { - return null; - } - - $sIcon = getIcon($aPlace); - - if (!isset($sIcon)) { - return null; - } - - return CONST_MapIcon_URL.'/'.$sIcon.'.p.20.png'; -} - -/** - * Return a class importance value for the given place. - * - * @param array[] $aPlace Information about the place. - * - * @return int An importance value. The lower the value, the more - * important the class. - */ -function getImportance($aPlace) -{ - static $aWithImportance = null; - - if ($aWithImportance === null) { - $aWithImportance = array_flip(array( - 'boundary:administrative', - 'place:country', - 'place:state', - 'place:province', - 'place:county', - 'place:city', - 'place:region', - 'place:island', - 'place:town', - 'place:village', - 'place:hamlet', - 'place:suburb', - 'place:locality', - 'landuse:farm', - 'place:farm', - 'highway:motorway_junction', - 'highway:motorway', - 'highway:trunk', - 'highway:primary', - 'highway:secondary', - 'highway:tertiary', - 'highway:residential', - 'highway:unclassified', - 'highway:living_street', - 'highway:service', - 'highway:track', - 'highway:road', - 'highway:byway', - 'highway:bridleway', - 'highway:cycleway', - 'highway:pedestrian', - 'highway:footway', - 'highway:steps', - 'highway:motorway_link', - 'highway:trunk_link', - 'highway:primary_link', - 'landuse:industrial', - 'landuse:residential', - 'landuse:retail', - 'landuse:commercial', - 'place:airport', - 'aeroway:aerodrome', - 'railway:station', - 'amenity:place_of_worship', - 'amenity:pub', - 'amenity:bar', - 'amenity:university', - 'tourism:museum', - 'amenity:arts_centre', - 'tourism:zoo', - 'tourism:theme_park', - 'tourism:attraction', - 'leisure:golf_course', - 'historic:castle', - 'amenity:hospital', - 'amenity:school', - 'amenity:theatre', - 'amenity:public_building', - 'amenity:library', - 'amenity:townhall', - 'amenity:community_centre', - 'amenity:fire_station', - 'amenity:police', - 'amenity:bank', - 'amenity:post_office', - 'leisure:park', - 'amenity:park', - 'landuse:park', - 'landuse:recreation_ground', - 'tourism:hotel', - 'tourism:motel', - 'amenity:cinema', - 'tourism:artwork', - 'historic:archaeological_site', - 'amenity:doctors', - 'leisure:sports_centre', - 'leisure:swimming_pool', - 'shop:supermarket', - 'shop:convenience', - 'amenity:restaurant', - 'amenity:fast_food', - 'amenity:cafe', - 'tourism:guest_house', - 'amenity:pharmacy', - 'amenity:fuel', - 'natural:peak', - 'waterway:waterfall', - 'natural:wood', - 'natural:water', - 'landuse:forest', - 'landuse:cemetery', - 'landuse:allotments', - 'landuse:farmyard', - 'railway:rail', - 'waterway:canal', - 'waterway:river', - 'waterway:stream', - 'shop:bicycle', - 'shop:clothes', - 'shop:hairdresser', - 'shop:doityourself', - 'shop:estate_agent', - 'shop:car', - 'shop:garden_centre', - 'shop:car_repair', - 'shop:newsagent', - 'shop:bakery', - 'shop:furniture', - 'shop:butcher', - 'shop:apparel', - 'shop:electronics', - 'shop:department_store', - 'shop:books', - 'shop:yes', - 'shop:outdoor', - 'shop:mall', - 'shop:florist', - 'shop:charity', - 'shop:hardware', - 'shop:laundry', - 'shop:shoes', - 'shop:beverages', - 'shop:dry_cleaning', - 'shop:carpet', - 'shop:computer', - 'shop:alcohol', - 'shop:optician', - 'shop:chemist', - 'shop:gallery', - 'shop:mobile_phone', - 'shop:sports', - 'shop:jewelry', - 'shop:pet', - 'shop:beauty', - 'shop:stationery', - 'shop:shopping_centre', - 'shop:general', - 'shop:electrical', - 'shop:toys', - 'shop:jeweller', - 'shop:betting', - 'shop:household', - 'shop:travel_agency', - 'shop:hifi', - 'amenity:shop', - 'tourism:information', - 'place:house', - 'place:house_name', - 'place:house_number', - 'place:country_code', - 'leisure:pitch', - 'highway:unsurfaced', - 'historic:ruins', - 'amenity:college', - 'historic:monument', - 'railway:subway', - 'historic:memorial', - 'leisure:nature_reserve', - 'leisure:common', - 'waterway:lock_gate', - 'natural:fell', - 'amenity:nightclub', - 'highway:path', - 'leisure:garden', - 'landuse:reservoir', - 'leisure:playground', - 'leisure:stadium', - 'historic:mine', - 'natural:cliff', - 'tourism:caravan_site', - 'amenity:bus_station', - 'amenity:kindergarten', - 'highway:construction', - 'amenity:atm', - 'amenity:emergency_phone', - 'waterway:lock', - 'waterway:riverbank', - 'natural:coastline', - 'tourism:viewpoint', - 'tourism:hostel', - 'tourism:bed_and_breakfast', - 'railway:halt', - 'railway:platform', - 'railway:tram', - 'amenity:courthouse', - 'amenity:recycling', - 'amenity:dentist', - 'natural:beach', - 'place:moor', - 'amenity:grave_yard', - 'waterway:drain', - 'landuse:grass', - 'landuse:village_green', - 'natural:bay', - 'railway:tram_stop', - 'leisure:marina', - 'highway:stile', - 'natural:moor', - 'railway:light_rail', - 'railway:narrow_gauge', - 'natural:land', - 'amenity:village_hall', - 'waterway:dock', - 'amenity:veterinary', - 'landuse:brownfield', - 'leisure:track', - 'railway:historic_station', - 'landuse:construction', - 'amenity:prison', - 'landuse:quarry', - 'amenity:telephone', - 'highway:traffic_signals', - 'natural:heath', - 'historic:house', - 'amenity:social_club', - 'landuse:military', - 'amenity:health_centre', - 'historic:building', - 'amenity:clinic', - 'highway:services', - 'amenity:ferry_terminal', - 'natural:marsh', - 'natural:hill', - 'highway:raceway', - 'amenity:taxi', - 'amenity:take_away', - 'amenity:car_rental', - 'place:islet', - 'amenity:nursery', - 'amenity:nursing_home', - 'amenity:toilets', - 'amenity:hall', - 'waterway:boatyard', - 'highway:mini_roundabout', - 'historic:manor', - 'tourism:chalet', - 'amenity:bicycle_parking', - 'amenity:hotel', - 'waterway:weir', - 'natural:wetland', - 'natural:cave_entrance', - 'amenity:crematorium', - 'tourism:picnic_site', - 'landuse:wood', - 'landuse:basin', - 'natural:tree', - 'leisure:slipway', - 'landuse:meadow', - 'landuse:piste', - 'amenity:care_home', - 'amenity:club', - 'amenity:medical_centre', - 'historic:roman_road', - 'historic:fort', - 'railway:subway_entrance', - 'historic:yes', - 'highway:gate', - 'leisure:fishing', - 'historic:museum', - 'amenity:car_wash', - 'railway:level_crossing', - 'leisure:bird_hide', - 'natural:headland', - 'tourism:apartments', - 'amenity:shopping', - 'natural:scrub', - 'natural:fen', - 'building:yes', - 'mountain_pass:yes', - 'amenity:parking', - 'highway:bus_stop', - 'place:postcode', - 'amenity:post_box', - 'place:houses', - 'railway:preserved', - 'waterway:derelict_canal', - 'amenity:dead_pub', - 'railway:disused_station', - 'railway:abandoned', - 'railway:disused' - )); - } - - $sClassPlace = $aPlace['class'].':'.$aPlace['type']; - - return $aWithImportance[$sClassPlace] ?? null; -} diff --git a/lib-php/DB.php b/lib-php/DB.php deleted file mode 100644 index 553d9452..00000000 --- a/lib-php/DB.php +++ /dev/null @@ -1,362 +0,0 @@ -sDSN = $sDSN ?? getSetting('DATABASE_DSN'); - } - - public function connect($bNew = false, $bPersistent = true) - { - if (isset($this->connection) && !$bNew) { - return true; - } - $aConnOptions = array( - \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, - \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, - \PDO::ATTR_PERSISTENT => $bPersistent - ); - - // https://secure.php.net/manual/en/ref.pdo-pgsql.connection.php - try { - $this->connection = new \PDO($this->sDSN, null, null, $aConnOptions); - } catch (\PDOException $e) { - $sMsg = 'Failed to establish database connection:' . $e->getMessage(); - throw new \Nominatim\DatabaseError($sMsg, 500, null, $e->getMessage()); - } - - $this->connection->exec("SET DateStyle TO 'sql,european'"); - $this->connection->exec("SET client_encoding TO 'utf-8'"); - // Disable JIT and parallel workers. They interfere badly with search SQL. - $this->connection->exec('SET max_parallel_workers_per_gather TO 0'); - if ($this->getPostgresVersion() >= 11) { - $this->connection->exec('SET jit_above_cost TO -1'); - } - - $iMaxExecution = ini_get('max_execution_time'); - if ($iMaxExecution > 0) { - $this->connection->setAttribute(\PDO::ATTR_TIMEOUT, $iMaxExecution); // seconds - } - - return true; - } - - // returns the number of rows that were modified or deleted by the SQL - // statement. If no rows were affected returns 0. - public function exec($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed') - { - $val = null; - try { - if (isset($aInputVars)) { - $stmt = $this->connection->prepare($sSQL); - $stmt->execute($aInputVars); - } else { - $val = $this->connection->exec($sSQL); - } - } catch (\PDOException $e) { - throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL); - } - return $val; - } - - /** - * Executes query. Returns first row as array. - * Returns false if no result found. - * - * @param string $sSQL - * - * @return array[] - */ - public function getRow($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed') - { - try { - $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage); - $row = $stmt->fetch(); - } catch (\PDOException $e) { - throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL); - } - return $row; - } - - /** - * Executes query. Returns first value of first result. - * Returns false if no results found. - * - * @param string $sSQL - * - * @return array[] - */ - public function getOne($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed') - { - try { - $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage); - $row = $stmt->fetch(\PDO::FETCH_NUM); - if ($row === false) { - return false; - } - } catch (\PDOException $e) { - throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL); - } - return $row[0]; - } - - /** - * Executes query. Returns array of results (arrays). - * Returns empty array if no results found. - * - * @param string $sSQL - * - * @return array[] - */ - public function getAll($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed') - { - try { - $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage); - $rows = $stmt->fetchAll(); - } catch (\PDOException $e) { - throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL); - } - return $rows; - } - - /** - * Executes query. Returns array of the first value of each result. - * Returns empty array if no results found. - * - * @param string $sSQL - * - * @return array[] - */ - public function getCol($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed') - { - $aVals = array(); - try { - $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage); - - while (($val = $stmt->fetchColumn(0)) !== false) { // returns first column or false - $aVals[] = $val; - } - } catch (\PDOException $e) { - throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL); - } - return $aVals; - } - - /** - * Executes query. Returns associate array mapping first value to second value of each result. - * Returns empty array if no results found. - * - * @param string $sSQL - * - * @return array[] - */ - public function getAssoc($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed') - { - try { - $stmt = $this->getQueryStatement($sSQL, $aInputVars, $sErrMessage); - - $aList = array(); - while ($aRow = $stmt->fetch(\PDO::FETCH_NUM)) { - $aList[$aRow[0]] = $aRow[1]; - } - } catch (\PDOException $e) { - throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL); - } - return $aList; - } - - /** - * Executes query. Returns a PDO statement to iterate over. - * - * @param string $sSQL - * - * @return PDOStatement - */ - public function getQueryStatement($sSQL, $aInputVars = null, $sErrMessage = 'Database query failed') - { - try { - if (isset($aInputVars)) { - $stmt = $this->connection->prepare($sSQL); - $stmt->execute($aInputVars); - } else { - $stmt = $this->connection->query($sSQL); - } - } catch (\PDOException $e) { - throw new \Nominatim\DatabaseError($sErrMessage, 500, null, $e, $sSQL); - } - return $stmt; - } - - /** - * St. John's Way => 'St. John\'s Way' - * - * @param string $sVal Text to be quoted. - * - * @return string - */ - public function getDBQuoted($sVal) - { - return $this->connection->quote($sVal); - } - - /** - * Like getDBQuoted, but takes an array. - * - * @param array $aVals List of text to be quoted. - * - * @return array[] - */ - public function getDBQuotedList($aVals) - { - return array_map(function ($sVal) { - return $this->getDBQuoted($sVal); - }, $aVals); - } - - /** - * [1,2,'b'] => 'ARRAY[1,2,'b']'' - * - * @param array $aVals List of text to be quoted. - * - * @return string - */ - public function getArraySQL($a) - { - return 'ARRAY['.join(',', $a).']'; - } - - /** - * Check if a table exists in the database. Returns true if it does. - * - * @param string $sTableName - * - * @return boolean - */ - public function tableExists($sTableName) - { - $sSQL = 'SELECT count(*) FROM pg_tables WHERE tablename = :tablename'; - return ($this->getOne($sSQL, array(':tablename' => $sTableName)) == 1); - } - - /** - * Deletes a table. Returns true if deleted or didn't exist. - * - * @param string $sTableName - * - * @return boolean - */ - public function deleteTable($sTableName) - { - return $this->exec('DROP TABLE IF EXISTS '.$sTableName.' CASCADE') == 0; - } - - /** - * Tries to connect to the database but on failure doesn't throw an exception. - * - * @return boolean - */ - public function checkConnection() - { - $bExists = true; - try { - $this->connect(true); - } catch (\Nominatim\DatabaseError $e) { - $bExists = false; - } - return $bExists; - } - - /** - * e.g. 9.6, 10, 11.2 - * - * @return float - */ - public function getPostgresVersion() - { - $sVersionString = $this->getOne('SHOW server_version_num'); - preg_match('#([0-9]?[0-9])([0-9][0-9])[0-9][0-9]#', $sVersionString, $aMatches); - return (float) ($aMatches[1].'.'.$aMatches[2]); - } - - /** - * e.g. 2, 2.2 - * - * @return float - */ - public function getPostgisVersion() - { - $sVersionString = $this->getOne('select postgis_lib_version()'); - preg_match('#^([0-9]+)[.]([0-9]+)[.]#', $sVersionString, $aMatches); - return (float) ($aMatches[1].'.'.$aMatches[2]); - } - - /** - * Returns an associate array of postgresql database connection settings. Keys can - * be 'database', 'hostspec', 'port', 'username', 'password'. - * Returns empty array on failure, thus check if at least 'database' is set. - * - * @return array[] - */ - public static function parseDSN($sDSN) - { - // https://secure.php.net/manual/en/ref.pdo-pgsql.connection.php - $aInfo = array(); - if (preg_match('/^pgsql:(.+)$/', $sDSN, $aMatches)) { - foreach (explode(';', $aMatches[1]) as $sKeyVal) { - list($sKey, $sVal) = explode('=', $sKeyVal, 2); - if ($sKey == 'host') { - $sKey = 'hostspec'; - } elseif ($sKey == 'dbname') { - $sKey = 'database'; - } elseif ($sKey == 'user') { - $sKey = 'username'; - } - $aInfo[$sKey] = $sVal; - } - } - return $aInfo; - } - - /** - * Takes an array of settings and return the DNS string. Key names can be - * 'database', 'hostspec', 'port', 'username', 'password' but aliases - * 'dbname', 'host' and 'user' are also supported. - * - * @return string - * - */ - public static function generateDSN($aInfo) - { - $sDSN = sprintf( - 'pgsql:host=%s;port=%s;dbname=%s;user=%s;password=%s;', - $aInfo['host'] ?? $aInfo['hostspec'] ?? '', - $aInfo['port'] ?? '', - $aInfo['dbname'] ?? $aInfo['database'] ?? '', - $aInfo['user'] ?? '', - $aInfo['password'] ?? '' - ); - $sDSN = preg_replace('/\b\w+=;/', '', $sDSN); - $sDSN = preg_replace('/;\Z/', '', $sDSN); - - return $sDSN; - } -} diff --git a/lib-php/DatabaseError.php b/lib-php/DatabaseError.php deleted file mode 100644 index 68f1efe6..00000000 --- a/lib-php/DatabaseError.php +++ /dev/null @@ -1,42 +0,0 @@ -oPDOErr = $oPDOErr; - $this->sSql = $sSql; - } - - public function __toString() - { - return __CLASS__ . ": [{$this->code}]: {$this->message}\n"; - } - - public function getSqlError() - { - return $this->oPDOErr->getMessage(); - } - - public function getSqlDebugDump() - { - if (CONST_Debug) { - return var_export($this->oPDOErr, true); - } else { - return $this->sSql; - } - } -} diff --git a/lib-php/DebugHtml.php b/lib-php/DebugHtml.php deleted file mode 100644 index 7b0cba2d..00000000 --- a/lib-php/DebugHtml.php +++ /dev/null @@ -1,189 +0,0 @@ -

Debug output for $sHeading

\n"; - } - - public static function newSection($sHeading) - { - echo "

$sHeading

\n"; - } - - public static function printVar($sHeading, $mVar) - { - echo '
'.$sHeading. ':  ';
-        Debug::outputVar($mVar, str_repeat(' ', strlen($sHeading) + 3));
-        echo "
\n"; - } - - public static function fmtArrayVals($aArr) - { - return array('__debug_format' => 'array_vals', 'data' => $aArr); - } - - public static function printDebugArray($sHeading, $oVar) - { - - if ($oVar === null) { - Debug::printVar($sHeading, 'null'); - } else { - Debug::printVar($sHeading, $oVar->debugInfo()); - } - } - - public static function printDebugTable($sHeading, $aVar) - { - echo ''.$sHeading.":\n"; - echo "\n"; - if (!empty($aVar)) { - echo " \n"; - $aKeys = array(); - $aInfo = reset($aVar); - if (!is_array($aInfo)) { - $aInfo = $aInfo->debugInfo(); - } - foreach ($aInfo as $sKey => $mVal) { - echo ' '."\n"; - $aKeys[] = $sKey; - } - echo " \n"; - foreach ($aVar as $oRow) { - $aInfo = $oRow; - if (!is_array($oRow)) { - $aInfo = $oRow->debugInfo(); - } - echo " \n"; - foreach ($aKeys as $sKey) { - echo ' '."\n"; - } - echo " \n"; - } - } - echo "
'.$sKey.'
';
-                    if (isset($aInfo[$sKey])) {
-                        Debug::outputVar($aInfo[$sKey], '');
-                    }
-                    echo '
\n"; - } - - public static function printGroupedSearch($aSearches, $aWordsIDs) - { - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - foreach ($aSearches as $aRankedSet) { - foreach ($aRankedSet as $aRow) { - $aRow->dumpAsHtmlTableRow($aWordsIDs); - } - } - echo '
rankName TokensName NotAddress TokensAddress Notcountryoperatorclasstypepostcodehousenumber
'; - } - - public static function printGroupTable($sHeading, $aVar) - { - echo ''.$sHeading.":\n"; - echo "\n"; - if (!empty($aVar)) { - echo " \n"; - echo ' '."\n"; - $aKeys = array(); - $aInfo = reset($aVar)[0]; - if (!is_array($aInfo)) { - $aInfo = $aInfo->debugInfo(); - } - foreach ($aInfo as $sKey => $mVal) { - echo ' '."\n"; - $aKeys[] = $sKey; - } - echo " \n"; - foreach ($aVar as $sGrpKey => $aGroup) { - foreach ($aGroup as $oRow) { - $aInfo = $oRow; - if (!is_array($oRow)) { - $aInfo = $oRow->debugInfo(); - } - echo " \n"; - echo ' '."\n"; - foreach ($aKeys as $sKey) { - echo ' '."\n"; - } - echo " \n"; - } - } - } - echo "
Group'.$sKey.'
'.$sGrpKey.'
';
-                        if (!empty($aInfo[$sKey])) {
-                            Debug::outputVar($aInfo[$sKey], '');
-                        }
-                        echo '
\n"; - } - - public static function printSQL($sSQL) - { - echo '

'.date('c').' '.htmlspecialchars($sSQL, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401).'

'."\n"; - } - - private static function outputVar($mVar, $sPreNL) - { - if (is_array($mVar) && !isset($mVar['__debug_format'])) { - $sPre = ''; - foreach ($mVar as $mKey => $aValue) { - echo $sPre; - $iKeyLen = Debug::outputSimpleVar($mKey); - echo ' => '; - Debug::outputVar( - $aValue, - $sPreNL.str_repeat(' ', $iKeyLen + 4) - ); - $sPre = "\n".$sPreNL; - } - } elseif (is_array($mVar) && isset($mVar['__debug_format'])) { - if (!empty($mVar['data'])) { - $sPre = ''; - foreach ($mVar['data'] as $mValue) { - echo $sPre; - Debug::outputSimpleVar($mValue); - $sPre = ', '; - } - } - } elseif (is_object($mVar) && method_exists($mVar, 'debugInfo')) { - Debug::outputVar($mVar->debugInfo(), $sPreNL); - } elseif (is_a($mVar, 'stdClass')) { - Debug::outputVar(json_decode(json_encode($mVar), true), $sPreNL); - } else { - Debug::outputSimpleVar($mVar); - } - } - - private static function outputSimpleVar($mVar) - { - if (is_bool($mVar)) { - echo ''.($mVar ? 'True' : 'False').''; - return $mVar ? 4 : 5; - } - - if (is_string($mVar)) { - $sOut = "'$mVar'"; - } else { - $sOut = (string)$mVar; - } - - echo htmlspecialchars($sOut, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401); - return strlen($sOut); - } -} diff --git a/lib-php/DebugNone.php b/lib-php/DebugNone.php deleted file mode 100644 index 818cc086..00000000 --- a/lib-php/DebugNone.php +++ /dev/null @@ -1,19 +0,0 @@ -oDB =& $oDB; - $this->oPlaceLookup = new PlaceLookup($this->oDB); - $this->oTokenizer = new \Nominatim\Tokenizer($this->oDB); - } - - public function setLanguagePreference($aLangPref) - { - $this->aLangPrefOrder = $aLangPref; - } - - public function getMoreUrlParams() - { - if ($this->aStructuredQuery) { - $aParams = $this->aStructuredQuery; - } else { - $aParams = array('q' => $this->sQuery); - } - - $aParams = array_merge($aParams, $this->oPlaceLookup->getMoreUrlParams()); - - if ($this->aExcludePlaceIDs) { - $aParams['exclude_place_ids'] = implode(',', $this->aExcludePlaceIDs); - } - - if ($this->bBoundedSearch) { - $aParams['bounded'] = '1'; - } - - if ($this->aCountryCodes) { - $aParams['countrycodes'] = implode(',', $this->aCountryCodes); - } - - if ($this->aViewBox) { - $aParams['viewbox'] = join(',', $this->aViewBox); - } - - return $aParams; - } - - public function setLimit($iLimit = 10) - { - if ($iLimit > 50) { - $iLimit = 50; - } elseif ($iLimit < 1) { - $iLimit = 1; - } - - $this->iFinalLimit = $iLimit; - $this->iLimit = $iLimit + max($iLimit, 10); - } - - public function setFeatureType($sFeatureType) - { - switch ($sFeatureType) { - case 'country': - $this->setRankRange(4, 4); - break; - case 'state': - $this->setRankRange(8, 8); - break; - case 'city': - $this->setRankRange(14, 16); - break; - case 'settlement': - $this->setRankRange(8, 20); - break; - } - } - - public function setRankRange($iMin, $iMax) - { - $this->iMinAddressRank = $iMin; - $this->iMaxAddressRank = $iMax; - } - - public function setViewbox($aViewbox) - { - $aBox = array_map('floatval', $aViewbox); - - $this->aViewBox[0] = max(-180.0, min($aBox[0], $aBox[2])); - $this->aViewBox[1] = max(-90.0, min($aBox[1], $aBox[3])); - $this->aViewBox[2] = min(180.0, max($aBox[0], $aBox[2])); - $this->aViewBox[3] = min(90.0, max($aBox[1], $aBox[3])); - - if ($this->aViewBox[2] - $this->aViewBox[0] < 0.000000001 - || $this->aViewBox[3] - $this->aViewBox[1] < 0.000000001 - ) { - userError("Bad parameter 'viewbox'. Not a box."); - } - } - - private function viewboxImportanceFactor($fX, $fY) - { - if (!$this->aViewBox) { - return 1; - } - - $fWidth = ($this->aViewBox[2] - $this->aViewBox[0])/2; - $fHeight = ($this->aViewBox[3] - $this->aViewBox[1])/2; - - $fXDist = abs($fX - ($this->aViewBox[0] + $this->aViewBox[2])/2); - $fYDist = abs($fY - ($this->aViewBox[1] + $this->aViewBox[3])/2); - - if ($fXDist <= $fWidth && $fYDist <= $fHeight) { - return 1; - } - - if ($fXDist <= $fWidth * 3 && $fYDist <= 3 * $fHeight) { - return 0.5; - } - - return 0.25; - } - - public function setQuery($sQueryString) - { - $this->sQuery = $sQueryString; - $this->aStructuredQuery = false; - } - - public function getQueryString() - { - return $this->sQuery; - } - - - public function loadParamArray($oParams, $sForceGeometryType = null) - { - $this->bBoundedSearch = $oParams->getBool('bounded', $this->bBoundedSearch); - - $this->setLimit($oParams->getInt('limit', $this->iFinalLimit)); - $this->iOffset = $oParams->getInt('offset', $this->iOffset); - - $this->bFallback = $oParams->getBool('fallback', $this->bFallback); - - // List of excluded Place IDs - used for more accurate pageing - $sExcluded = $oParams->getStringList('exclude_place_ids'); - if ($sExcluded) { - foreach ($sExcluded as $iExcludedPlaceID) { - $iExcludedPlaceID = (int)$iExcludedPlaceID; - if ($iExcludedPlaceID) { - $aExcludePlaceIDs[$iExcludedPlaceID] = $iExcludedPlaceID; - } - } - - if (isset($aExcludePlaceIDs)) { - $this->aExcludePlaceIDs = $aExcludePlaceIDs; - } - } - - // Only certain ranks of feature - $sFeatureType = $oParams->getString('featureType'); - if (!$sFeatureType) { - $sFeatureType = $oParams->getString('featuretype'); - } - if ($sFeatureType) { - $this->setFeatureType($sFeatureType); - } - - // Country code list - $sCountries = $oParams->getStringList('countrycodes'); - if ($sCountries) { - foreach ($sCountries as $sCountryCode) { - if (preg_match('/^[a-zA-Z][a-zA-Z]$/', $sCountryCode)) { - $aCountries[] = strtolower($sCountryCode); - } - } - if (isset($aCountries)) { - $this->aCountryCodes = $aCountries; - } - } - - $aViewbox = $oParams->getStringList('viewboxlbrt'); - if ($aViewbox) { - if (count($aViewbox) != 4) { - userError("Bad parameter 'viewboxlbrt'. Expected 4 coordinates."); - } - $this->setViewbox($aViewbox); - } else { - $aViewbox = $oParams->getStringList('viewbox'); - if ($aViewbox) { - if (count($aViewbox) != 4) { - userError("Bad parameter 'viewbox'. Expected 4 coordinates."); - } - $this->setViewBox($aViewbox); - } else { - $aRoute = $oParams->getStringList('route'); - $fRouteWidth = $oParams->getFloat('routewidth'); - if ($aRoute && $fRouteWidth) { - $this->aRoutePoints = $aRoute; - $this->aRouteWidth = $fRouteWidth; - } - } - } - - $this->oPlaceLookup->loadParamArray($oParams, $sForceGeometryType); - $this->oPlaceLookup->setIncludeAddressDetails($oParams->getBool('addressdetails', false)); - } - - public function setQueryFromParams($oParams) - { - // Search query - $this->setStructuredQuery( - $oParams->getString('amenity'), - $oParams->getString('street'), - $oParams->getString('city'), - $oParams->getString('county'), - $oParams->getString('state'), - $oParams->getString('country'), - $oParams->getString('postalcode') - ); - if (!$this->sQuery) { - $sQuery = $oParams->getString('q'); - - if ($sQuery) { - $this->setQuery($sQuery); - } - } - } - - public function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues) - { - $sValue = trim($sValue); - if (!$sValue) { - return false; - } - $this->aStructuredQuery[$sKey] = $sValue; - if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30) { - $this->iMinAddressRank = $iNewMinAddressRank; - $this->iMaxAddressRank = $iNewMaxAddressRank; - } - if ($aItemListValues) { - $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues); - } - return true; - } - - public function setStructuredQuery($sAmenity = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false) - { - $this->sQuery = false; - - if ($sAmenity || $sStreet || $sCity || $sCounty || $sState || $sCountry || $sPostalCode) { - // Reset - $this->iMinAddressRank = 0; - $this->iMaxAddressRank = 30; - $this->aAddressRankList = array(); - - $this->aStructuredQuery = array(); - $this->sAllowedTypesSQLList = false; - - $this->loadStructuredAddressElement($sAmenity, 'amenity', 26, 30, false); - $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false); - $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false); - $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false); - $this->loadStructuredAddressElement($sState, 'state', 8, 8, false); - $this->loadStructuredAddressElement($sPostalCode, 'postalcode', 5, 11, array(5, 11)); - $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false); - - if (!empty($this->aStructuredQuery)) { - $this->sQuery = join(', ', $this->aStructuredQuery); - if ($this->iMaxAddressRank < 30) { - $this->sAllowedTypesSQLList = '(\'place\',\'boundary\')'; - } - } - } - } - - public function fallbackStructuredQuery() - { - $aParams = $this->aStructuredQuery; - - if (!$aParams || count($aParams) == 1) { - return false; - } - - $aOrderToFallback = array('postalcode', 'street', 'city', 'county', 'state'); - - foreach ($aOrderToFallback as $sType) { - if (isset($aParams[$sType])) { - unset($aParams[$sType]); - $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']); - return true; - } - } - - return false; - } - - public function getGroupedSearches($aSearches, $aPhrases, $oValidTokens) - { - /* - Calculate all searches using oValidTokens i.e. - 'Wodsworth Road, Sheffield' => - - Phrase Wordset - 0 0 (wodsworth road) - 0 1 (wodsworth)(road) - 1 0 (sheffield) - - Score how good the search is so they can be ordered - */ - foreach ($aPhrases as $iPhrase => $oPhrase) { - $aNewPhraseSearches = array(); - $oPosition = new SearchPosition( - $oPhrase->getPhraseType(), - $iPhrase, - count($aPhrases) - ); - - foreach ($oPhrase->getWordSets() as $aWordset) { - $aWordsetSearches = $aSearches; - - // Add all words from this wordset - foreach ($aWordset as $iToken => $sToken) { - $aNewWordsetSearches = array(); - $oPosition->setTokenPosition($iToken, count($aWordset)); - - foreach ($aWordsetSearches as $oCurrentSearch) { - foreach ($oValidTokens->get($sToken) as $oSearchTerm) { - if ($oSearchTerm->isExtendable($oCurrentSearch, $oPosition)) { - $aNewSearches = $oSearchTerm->extendSearch( - $oCurrentSearch, - $oPosition - ); - - foreach ($aNewSearches as $oSearch) { - if ($oSearch->getRank() < $this->iMaxRank) { - $aNewWordsetSearches[] = $oSearch; - } - } - } - } - } - // Sort and cut - usort($aNewWordsetSearches, array('Nominatim\SearchDescription', 'bySearchRank')); - $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50); - } - - $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches); - usort($aNewPhraseSearches, array('Nominatim\SearchDescription', 'bySearchRank')); - - $aSearchHash = array(); - foreach ($aNewPhraseSearches as $iSearch => $aSearch) { - $sHash = serialize($aSearch); - if (isset($aSearchHash[$sHash])) { - unset($aNewPhraseSearches[$iSearch]); - } else { - $aSearchHash[$sHash] = 1; - } - } - - $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50); - } - - // Re-group the searches by their score, junk anything over 20 as just not worth trying - $aGroupedSearches = array(); - foreach ($aNewPhraseSearches as $aSearch) { - $iRank = $aSearch->getRank(); - if ($iRank < $this->iMaxRank) { - if (!isset($aGroupedSearches[$iRank])) { - $aGroupedSearches[$iRank] = array(); - } - $aGroupedSearches[$iRank][] = $aSearch; - } - } - ksort($aGroupedSearches); - - $iSearchCount = 0; - $aSearches = array(); - foreach ($aGroupedSearches as $aNewSearches) { - $iSearchCount += count($aNewSearches); - $aSearches = array_merge($aSearches, $aNewSearches); - if ($iSearchCount > 50) { - break; - } - } - } - - // Revisit searches, drop bad searches and give penalty to unlikely combinations. - $aGroupedSearches = array(); - foreach ($aSearches as $oSearch) { - if (!$oSearch->isValidSearch()) { - continue; - } - - $iRank = $oSearch->getRank(); - if (!isset($aGroupedSearches[$iRank])) { - $aGroupedSearches[$iRank] = array(); - } - $aGroupedSearches[$iRank][] = $oSearch; - } - ksort($aGroupedSearches); - - return $aGroupedSearches; - } - - /* Perform the actual query lookup. - - Returns an ordered list of results, each with the following fields: - osm_type: type of corresponding OSM object - N - node - W - way - R - relation - P - postcode (internally computed) - osm_id: id of corresponding OSM object - class: general object class (corresponds to tag key of primary OSM tag) - type: subclass of object (corresponds to tag value of primary OSM tag) - admin_level: see https://wiki.openstreetmap.org/wiki/Admin_level - rank_search: rank in search hierarchy - (see also https://wiki.openstreetmap.org/wiki/Nominatim/Development_overview#Country_to_street_level) - rank_address: rank in address hierarchy (determines orer in address) - place_id: internal key (may differ between different instances) - country_code: ISO country code - langaddress: localized full address - placename: localized name of object - ref: content of ref tag (if available) - lon: longitude - lat: latitude - importance: importance of place based on Wikipedia link count - addressimportance: cumulated importance of address elements - extra_place: type of place (for admin boundaries, if there is a place tag) - aBoundingBox: bounding Box - label: short description of the object class/type (English only) - name: full name (currently the same as langaddress) - foundorder: secondary ordering for places with same importance - */ - - - public function lookup() - { - Debug::newFunction('Geocode::lookup'); - if (!$this->sQuery && !$this->aStructuredQuery) { - return array(); - } - - Debug::printDebugArray('Geocode', $this); - - $oCtx = new SearchContext(); - - if ($this->aRoutePoints) { - $oCtx->setViewboxFromRoute( - $this->oDB, - $this->aRoutePoints, - $this->aRouteWidth, - $this->bBoundedSearch - ); - } elseif ($this->aViewBox) { - $oCtx->setViewboxFromBox($this->aViewBox, $this->bBoundedSearch); - } - if ($this->aExcludePlaceIDs) { - $oCtx->setExcludeList($this->aExcludePlaceIDs); - } - if ($this->aCountryCodes) { - $oCtx->setCountryList($this->aCountryCodes); - } - - Debug::newSection('Query Preprocessing'); - - $sQuery = $this->sQuery; - if (!preg_match('//u', $sQuery)) { - userError('Query string is not UTF-8 encoded.'); - } - - // Do we have anything that looks like a lat/lon pair? - $sQuery = $oCtx->setNearPointFromQuery($sQuery); - - if ($sQuery || $this->aStructuredQuery) { - // Start with a single blank search - $aSearches = array(new SearchDescription($oCtx)); - - if ($sQuery) { - $sQuery = $aSearches[0]->extractKeyValuePairs($sQuery); - } - - $sSpecialTerm = ''; - if ($sQuery) { - preg_match_all( - '/\\[([\\w ]*)\\]/u', - $sQuery, - $aSpecialTermsRaw, - PREG_SET_ORDER - ); - if (!empty($aSpecialTermsRaw)) { - Debug::printVar('Special terms', $aSpecialTermsRaw); - } - - foreach ($aSpecialTermsRaw as $aSpecialTerm) { - $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery); - if (!$sSpecialTerm) { - $sSpecialTerm = $aSpecialTerm[1]; - } - } - } - if (!$sSpecialTerm && $this->aStructuredQuery - && isset($this->aStructuredQuery['amenity'])) { - $sSpecialTerm = $this->aStructuredQuery['amenity']; - unset($this->aStructuredQuery['amenity']); - } - - if ($sSpecialTerm && !$aSearches[0]->hasOperator()) { - $aTokens = $this->oTokenizer->tokensForSpecialTerm($sSpecialTerm); - - if (!empty($aTokens)) { - $aNewSearches = array(); - $oPosition = new SearchPosition('', 0, 1); - $oPosition->setTokenPosition(0, 1); - - foreach ($aSearches as $oSearch) { - foreach ($aTokens as $oToken) { - $aNewSearches = array_merge( - $aNewSearches, - $oToken->extendSearch($oSearch, $oPosition) - ); - } - } - $aSearches = $aNewSearches; - } - } - - // Split query into phrases - // Commas are used to reduce the search space by indicating where phrases split - $aPhrases = array(); - if ($this->aStructuredQuery) { - foreach ($this->aStructuredQuery as $iPhrase => $sPhrase) { - $aPhrases[] = new Phrase($sPhrase, $iPhrase); - } - } else { - foreach (explode(',', $sQuery) as $sPhrase) { - $aPhrases[] = new Phrase($sPhrase, ''); - } - } - - Debug::printDebugArray('Search context', $oCtx); - Debug::printDebugArray('Base search', empty($aSearches) ? null : $aSearches[0]); - - Debug::newSection('Tokenization'); - $oValidTokens = $this->oTokenizer->extractTokensFromPhrases($aPhrases); - - if ($oValidTokens->count() > 0) { - $oCtx->setFullNameWords($oValidTokens->getFullWordIDs()); - - $aPhrases = array_filter($aPhrases, function ($oPhrase) { - return $oPhrase->getWordSets() !== null; - }); - - // Any words that have failed completely? - // TODO: suggestions - - Debug::printGroupTable('Valid Tokens', $oValidTokens->debugInfo()); - Debug::printDebugTable('Phrases', $aPhrases); - - Debug::newSection('Search candidates'); - - $aGroupedSearches = $this->getGroupedSearches($aSearches, $aPhrases, $oValidTokens); - - if (!$this->aStructuredQuery) { - // Reverse phrase array and also reverse the order of the wordsets in - // the first and final phrase. Don't bother about phrases in the middle - // because order in the address doesn't matter. - $aPhrases = array_reverse($aPhrases); - $aPhrases[0]->invertWordSets(); - if (count($aPhrases) > 1) { - $aPhrases[count($aPhrases)-1]->invertWordSets(); - } - $aReverseGroupedSearches = $this->getGroupedSearches($aSearches, $aPhrases, $oValidTokens); - - foreach ($aReverseGroupedSearches as $aSearches) { - foreach ($aSearches as $aSearch) { - if (!isset($aGroupedSearches[$aSearch->getRank()])) { - $aGroupedSearches[$aSearch->getRank()] = array(); - } - $aGroupedSearches[$aSearch->getRank()][] = $aSearch; - } - } - - ksort($aGroupedSearches); - } - } else { - // Re-group the searches by their score, junk anything over 20 as just not worth trying - $aGroupedSearches = array(); - foreach ($aSearches as $aSearch) { - if ($aSearch->getRank() < $this->iMaxRank) { - if (!isset($aGroupedSearches[$aSearch->getRank()])) { - $aGroupedSearches[$aSearch->getRank()] = array(); - } - $aGroupedSearches[$aSearch->getRank()][] = $aSearch; - } - } - ksort($aGroupedSearches); - } - - // Filter out duplicate searches - $aSearchHash = array(); - foreach ($aGroupedSearches as $iGroup => $aSearches) { - foreach ($aSearches as $iSearch => $aSearch) { - $sHash = serialize($aSearch); - if (isset($aSearchHash[$sHash])) { - unset($aGroupedSearches[$iGroup][$iSearch]); - if (empty($aGroupedSearches[$iGroup])) { - unset($aGroupedSearches[$iGroup]); - } - } else { - $aSearchHash[$sHash] = 1; - } - } - } - - Debug::printGroupedSearch( - $aGroupedSearches, - $oValidTokens->debugTokenByWordIdList() - ); - - // Start the search process - $iGroupLoop = 0; - $iQueryLoop = 0; - $aNextResults = array(); - foreach ($aGroupedSearches as $iGroupedRank => $aSearches) { - $iGroupLoop++; - $aResults = $aNextResults; - foreach ($aSearches as $oSearch) { - $iQueryLoop++; - - Debug::newSection("Search Loop, group $iGroupLoop, loop $iQueryLoop"); - Debug::printGroupedSearch( - array($iGroupedRank => array($oSearch)), - $oValidTokens->debugTokenByWordIdList() - ); - - $aNewResults = $oSearch->query( - $this->oDB, - $this->iMinAddressRank, - $this->iMaxAddressRank, - $this->iLimit - ); - - // The same result may appear in different rounds, only - // use the one with minimal rank. - foreach ($aNewResults as $iPlace => $oRes) { - if (!isset($aResults[$iPlace]) - || $aResults[$iPlace]->iResultRank > $oRes->iResultRank) { - $aResults[$iPlace] = $oRes; - } - } - - if ($iQueryLoop > 30) { - break; - } - } - - if (!empty($aResults)) { - $aSplitResults = Result::splitResults($aResults); - Debug::printVar('Split results', $aSplitResults); - if ($iGroupLoop <= 4 - && reset($aSplitResults['head'])->iResultRank > 0 - && $iGroupedRank !== array_key_last($aGroupedSearches)) { - // Haven't found an exact match for the query yet. - // Therefore add result from the next group level. - $aNextResults = $aSplitResults['head']; - foreach ($aNextResults as $oRes) { - $oRes->iResultRank--; - } - foreach ($aSplitResults['tail'] as $oRes) { - $oRes->iResultRank--; - $aNextResults[$oRes->iId] = $oRes; - } - $aResults = array(); - } else { - $aResults = $aSplitResults['head']; - } - } - - if (!empty($aResults) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30)) { - // Need to verify passes rank limits before dropping out of the loop (yuk!) - // reduces the number of place ids, like a filter - // rank_address is 30 for interpolated housenumbers - $aFilterSql = array(); - $sPlaceIds = Result::joinIdsByTable($aResults, Result::TABLE_PLACEX); - if ($sPlaceIds) { - $sSQL = 'SELECT place_id FROM placex '; - $sSQL .= 'WHERE place_id in ('.$sPlaceIds.') '; - $sSQL .= ' AND ('; - $sSQL .= " placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank "; - $sSQL .= " OR placex.rank_search between $this->iMinAddressRank and $this->iMaxAddressRank "; - if ($this->aAddressRankList) { - $sSQL .= ' OR placex.rank_address in ('.join(',', $this->aAddressRankList).')'; - } - $sSQL .= ')'; - $aFilterSql[] = $sSQL; - } - $sPlaceIds = Result::joinIdsByTable($aResults, Result::TABLE_POSTCODE); - if ($sPlaceIds) { - $sSQL = ' SELECT place_id FROM location_postcode lp '; - $sSQL .= 'WHERE place_id in ('.$sPlaceIds.') '; - $sSQL .= " AND (lp.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank "; - if ($this->aAddressRankList) { - $sSQL .= ' OR lp.rank_address in ('.join(',', $this->aAddressRankList).')'; - } - $sSQL .= ') '; - $aFilterSql[] = $sSQL; - } - - $aFilteredIDs = array(); - if ($aFilterSql) { - $sSQL = join(' UNION ', $aFilterSql); - Debug::printSQL($sSQL); - $aFilteredIDs = $this->oDB->getCol($sSQL); - } - - $tempIDs = array(); - foreach ($aResults as $oResult) { - if (($this->iMaxAddressRank == 30 && - ($oResult->iTable == Result::TABLE_OSMLINE - || $oResult->iTable == Result::TABLE_TIGER)) - || in_array($oResult->iId, $aFilteredIDs) - ) { - $tempIDs[$oResult->iId] = $oResult; - } - } - $aResults = $tempIDs; - } - - if (!empty($aResults) || $iGroupLoop > 6 || $iQueryLoop > 40) { - break; - } - } - } else { - // Just interpret as a reverse geocode - $oReverse = new ReverseGeocode($this->oDB); - $oReverse->setZoom(18); - - $oLookup = $oReverse->lookupPoint($oCtx->sqlNear, false); - - Debug::printVar('Reverse search', $oLookup); - - if ($oLookup) { - $aResults = array($oLookup->iId => $oLookup); - } - } - - // No results? Done - if (empty($aResults)) { - if ($this->bFallback && $this->fallbackStructuredQuery()) { - return $this->lookup(); - } - - return array(); - } - - if ($this->aAddressRankList) { - $this->oPlaceLookup->setAddressRankList($this->aAddressRankList); - } - $this->oPlaceLookup->setAllowedTypesSQLList($this->sAllowedTypesSQLList); - $this->oPlaceLookup->setLanguagePreference($this->aLangPrefOrder); - if ($oCtx->hasNearPoint()) { - $this->oPlaceLookup->setAnchorSql($oCtx->sqlNear); - } - - $aSearchResults = $this->oPlaceLookup->lookup($aResults); - - $aRecheckWords = preg_split('/\b[\s,\\-]*/u', $sQuery); - foreach ($aRecheckWords as $i => $sWord) { - if (!preg_match('/[\pL\pN]/', $sWord)) { - unset($aRecheckWords[$i]); - } - } - - Debug::printVar('Recheck words', $aRecheckWords); - - foreach ($aSearchResults as $iIdx => $aResult) { - $fRadius = ClassTypes\getDefRadius($aResult); - - $aOutlineResult = $this->oPlaceLookup->getOutlines($aResult['place_id'], $aResult['lon'], $aResult['lat'], $fRadius); - if ($aOutlineResult) { - $aResult = array_merge($aResult, $aOutlineResult); - } - - // Is there an icon set for this type of result? - $sIcon = ClassTypes\getIconFile($aResult); - if (isset($sIcon)) { - $aResult['icon'] = $sIcon; - } - - $sLabel = ClassTypes\getLabel($aResult); - if (isset($sLabel)) { - $aResult['label'] = $sLabel; - } - $aResult['name'] = $aResult['langaddress']; - - if ($oCtx->hasNearPoint()) { - $aResult['importance'] = 0.001; - $aResult['foundorder'] = $aResult['addressimportance']; - } else { - if ($aResult['importance'] == 0) { - $aResult['importance'] = 0.0001; - } - $aResult['importance'] *= $this->viewboxImportanceFactor( - $aResult['lon'], - $aResult['lat'] - ); - - // secondary ordering (for results with same importance (the smaller the better): - // - approximate importance of address parts - if (isset($aResult['addressimportance']) && $aResult['addressimportance']) { - $aResult['foundorder'] = -$aResult['addressimportance']/10; - } else { - $aResult['foundorder'] = -$aResult['importance']; - } - // - number of exact matches from the query - $aResult['foundorder'] -= $aResults[$aResult['place_id']]->iExactMatches; - // - importance of the class/type - $iClassImportance = ClassTypes\getImportance($aResult); - if (isset($iClassImportance)) { - $aResult['foundorder'] += 0.0001 * $iClassImportance; - } else { - $aResult['foundorder'] += 0.01; - } - // - rank - $aResult['foundorder'] -= 0.00001 * (30 - $aResult['rank_search']); - - // Adjust importance for the number of exact string matches in the result - $iCountWords = 0; - $sAddress = $aResult['langaddress']; - foreach ($aRecheckWords as $i => $sWord) { - if (grapheme_stripos($sAddress, $sWord)!==false) { - $iCountWords++; - if (preg_match('/(^|,)\s*'.preg_quote($sWord, '/').'\s*(,|$)/', $sAddress)) { - $iCountWords += 0.1; - } - } - } - - // 0.1 is a completely arbitrary number but something in the range 0.1 to 0.5 would seem right - $aResult['importance'] = $aResult['importance'] + ($iCountWords*0.1); - } - $aSearchResults[$iIdx] = $aResult; - } - uasort($aSearchResults, 'byImportance'); - Debug::printVar('Pre-filter results', $aSearchResults); - - $aOSMIDDone = array(); - $aClassTypeNameDone = array(); - $aToFilter = $aSearchResults; - $aSearchResults = array(); - - foreach ($aToFilter as $aResult) { - $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id']; - if (!$this->oPlaceLookup->doDeDupe() || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']]) - && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])) - ) { - $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true; - $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true; - $aSearchResults[] = $aResult; - } - - // Absolute limit on number of results - if (count($aSearchResults) >= $this->iFinalLimit) { - break; - } - } - - Debug::printVar('Post-filter results', $aSearchResults); - return $aSearchResults; - } // end lookup() - - public function debugInfo() - { - return array( - 'Query' => $this->sQuery, - 'Structured query' => $this->aStructuredQuery, - 'Name keys' => Debug::fmtArrayVals($this->aLangPrefOrder), - 'Excluded place IDs' => Debug::fmtArrayVals($this->aExcludePlaceIDs), - 'Limit (for searches)' => $this->iLimit, - 'Limit (for results)'=> $this->iFinalLimit, - 'Country codes' => Debug::fmtArrayVals($this->aCountryCodes), - 'Bounded search' => $this->bBoundedSearch, - 'Viewbox' => Debug::fmtArrayVals($this->aViewBox), - 'Route points' => Debug::fmtArrayVals($this->aRoutePoints), - 'Route width' => $this->aRouteWidth, - 'Max rank' => $this->iMaxRank, - 'Min address rank' => $this->iMinAddressRank, - 'Max address rank' => $this->iMaxAddressRank, - 'Address rank list' => Debug::fmtArrayVals($this->aAddressRankList) - ); - } -} // end class diff --git a/lib-php/ParameterParser.php b/lib-php/ParameterParser.php deleted file mode 100644 index a4936d37..00000000 --- a/lib-php/ParameterParser.php +++ /dev/null @@ -1,157 +0,0 @@ -aParams = ($aParams === null) ? $_GET : $aParams; - } - - public function getBool($sName, $bDefault = false) - { - if (!isset($this->aParams[$sName]) - || !is_string($this->aParams[$sName]) - || strlen($this->aParams[$sName]) == 0 - ) { - return $bDefault; - } - - return (bool) $this->aParams[$sName]; - } - - public function getInt($sName, $bDefault = false) - { - if (!isset($this->aParams[$sName]) || is_array($this->aParams[$sName])) { - return $bDefault; - } - - if (!preg_match('/^[+-]?[0-9]+$/', $this->aParams[$sName])) { - userError("Integer number expected for parameter '$sName'"); - } - - return (int) $this->aParams[$sName]; - } - - public function getFloat($sName, $bDefault = false) - { - if (!isset($this->aParams[$sName]) || is_array($this->aParams[$sName])) { - return $bDefault; - } - - if (!preg_match('/^[+-]?[0-9]*\.?[0-9]+$/', $this->aParams[$sName])) { - userError("Floating-point number expected for parameter '$sName'"); - } - - return (float) $this->aParams[$sName]; - } - - public function getString($sName, $bDefault = false) - { - if (!isset($this->aParams[$sName]) - || !is_string($this->aParams[$sName]) - || strlen($this->aParams[$sName]) == 0 - ) { - return $bDefault; - } - - return $this->aParams[$sName]; - } - - public function getSet($sName, $aValues, $sDefault = false) - { - if (!isset($this->aParams[$sName]) - || !is_string($this->aParams[$sName]) - || strlen($this->aParams[$sName]) == 0 - ) { - return $sDefault; - } - - if (!in_array($this->aParams[$sName], $aValues, true)) { - userError("Parameter '$sName' must be one of: ".join(', ', $aValues)); - } - - return $this->aParams[$sName]; - } - - public function getStringList($sName, $aDefault = false) - { - $sValue = $this->getString($sName); - - if ($sValue) { - // removes all NULL, FALSE and Empty Strings but leaves 0 (zero) values - return array_values(array_filter(explode(',', $sValue), 'strlen')); - } - - return $aDefault; - } - - public function getPreferredLanguages($sFallback = null) - { - if ($sFallback === null && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { - $sFallback = $_SERVER['HTTP_ACCEPT_LANGUAGE']; - } - - $aLanguages = array(); - $sLangString = $this->getString('accept-language', $sFallback); - - if ($sLangString - && preg_match_all('/(([a-z]{1,8})([-_][a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i', $sLangString, $aLanguagesParse, PREG_SET_ORDER) - ) { - foreach ($aLanguagesParse as $iLang => $aLanguage) { - $aLanguages[$aLanguage[1]] = isset($aLanguage[5])?(float)$aLanguage[5]:1 - ($iLang/100); - if (!isset($aLanguages[$aLanguage[2]])) { - $aLanguages[$aLanguage[2]] = $aLanguages[$aLanguage[1]]/10; - } - } - arsort($aLanguages); - } - if (empty($aLanguages) && CONST_Default_Language) { - $aLanguages[CONST_Default_Language] = 1; - } - - foreach ($aLanguages as $sLanguage => $fLanguagePref) { - $this->addNameTag($aLangPrefOrder, 'name:'.$sLanguage); - } - $this->addNameTag($aLangPrefOrder, 'name'); - $this->addNameTag($aLangPrefOrder, 'brand'); - foreach ($aLanguages as $sLanguage => $fLanguagePref) { - $this->addNameTag($aLangPrefOrder, 'official_name:'.$sLanguage); - $this->addNameTag($aLangPrefOrder, 'short_name:'.$sLanguage); - } - $this->addNameTag($aLangPrefOrder, 'official_name'); - $this->addNameTag($aLangPrefOrder, 'short_name'); - $this->addNameTag($aLangPrefOrder, 'ref'); - $this->addNameTag($aLangPrefOrder, 'type'); - return $aLangPrefOrder; - } - - private function addNameTag(&$aLangPrefOrder, $sTag) - { - $aLangPrefOrder[$sTag] = $sTag; - $aLangPrefOrder['_place_'.$sTag] = '_place_'.$sTag; - } - - public function hasSetAny($aParamNames) - { - foreach ($aParamNames as $sName) { - if ($this->getBool($sName)) { - return true; - } - } - - return false; - } -} diff --git a/lib-php/Phrase.php b/lib-php/Phrase.php deleted file mode 100644 index 4ed4d402..00000000 --- a/lib-php/Phrase.php +++ /dev/null @@ -1,89 +0,0 @@ -sPhrase = trim($sPhrase); - $this->sPhraseType = $sPhraseType; - } - - /** - * Get the original phrase of the string. - */ - public function getPhrase() - { - return $this->sPhrase; - } - - /** - * Return the element type of the phrase. - * - * @return string Pharse type if the phrase comes from a structured query - * or empty string otherwise. - */ - public function getPhraseType() - { - return $this->sPhraseType; - } - - public function setWordSets($aWordSets) - { - $this->aWordSets = $aWordSets; - } - - /** - * Return the array of possible segmentations of the phrase. - * - * @return string[][] Array of segmentations, each consisting of an - * array of terms. - */ - public function getWordSets() - { - return $this->aWordSets; - } - - /** - * Invert the set of possible segmentations. - * - * @return void - */ - public function invertWordSets() - { - foreach ($this->aWordSets as $i => $aSet) { - $this->aWordSets[$i] = array_reverse($aSet); - } - } - - public function debugInfo() - { - return array( - 'Type' => $this->sPhraseType, - 'Phrase' => $this->sPhrase, - 'WordSets' => $this->aWordSets - ); - } -} diff --git a/lib-php/PlaceLookup.php b/lib-php/PlaceLookup.php deleted file mode 100644 index 895a30df..00000000 --- a/lib-php/PlaceLookup.php +++ /dev/null @@ -1,615 +0,0 @@ -oDB =& $oDB; - } - - public function doDeDupe() - { - return $this->bDeDupe; - } - - public function setIncludeAddressDetails($b) - { - $this->bAddressDetails = $b; - } - - public function loadParamArray($oParams, $sGeomType = null) - { - $aLangs = $oParams->getPreferredLanguages(); - $this->aLangPrefOrderSql = - 'ARRAY['.join(',', $this->oDB->getDBQuotedList($aLangs)).']'; - - $this->bExtraTags = $oParams->getBool('extratags', false); - $this->bNameDetails = $oParams->getBool('namedetails', false); - - $this->bDeDupe = $oParams->getBool('dedupe', $this->bDeDupe); - - if ($sGeomType === null || $sGeomType == 'geojson') { - $this->bIncludePolygonAsGeoJSON = $oParams->getBool('polygon_geojson'); - } - - if ($oParams->getString('format', '') !== 'geojson') { - if ($sGeomType === null || $sGeomType == 'text') { - $this->bIncludePolygonAsText = $oParams->getBool('polygon_text'); - } - if ($sGeomType === null || $sGeomType == 'kml') { - $this->bIncludePolygonAsKML = $oParams->getBool('polygon_kml'); - } - if ($sGeomType === null || $sGeomType == 'svg') { - $this->bIncludePolygonAsSVG = $oParams->getBool('polygon_svg'); - } - } - $this->fPolygonSimplificationThreshold - = $oParams->getFloat('polygon_threshold', 0.0); - - $iWantedTypes = - ($this->bIncludePolygonAsText ? 1 : 0) + - ($this->bIncludePolygonAsGeoJSON ? 1 : 0) + - ($this->bIncludePolygonAsKML ? 1 : 0) + - ($this->bIncludePolygonAsSVG ? 1 : 0); - if ($iWantedTypes > CONST_PolygonOutput_MaximumTypes) { - if (CONST_PolygonOutput_MaximumTypes) { - userError('Select only '.CONST_PolygonOutput_MaximumTypes.' polygon output option'); - } else { - userError('Polygon output is disabled'); - } - } - } - - public function getMoreUrlParams() - { - $aParams = array(); - - if ($this->bAddressDetails) { - $aParams['addressdetails'] = '1'; - } - if ($this->bExtraTags) { - $aParams['extratags'] = '1'; - } - if ($this->bNameDetails) { - $aParams['namedetails'] = '1'; - } - - if ($this->bIncludePolygonAsText) { - $aParams['polygon_text'] = '1'; - } - if ($this->bIncludePolygonAsGeoJSON) { - $aParams['polygon_geojson'] = '1'; - } - if ($this->bIncludePolygonAsKML) { - $aParams['polygon_kml'] = '1'; - } - if ($this->bIncludePolygonAsSVG) { - $aParams['polygon_svg'] = '1'; - } - - if ($this->fPolygonSimplificationThreshold > 0.0) { - $aParams['polygon_threshold'] = $this->fPolygonSimplificationThreshold; - } - - if (!$this->bDeDupe) { - $aParams['dedupe'] = '0'; - } - - return $aParams; - } - - public function setAnchorSql($sPoint) - { - $this->sAnchorSql = $sPoint; - } - - public function setAddressRankList($aList) - { - $this->sAddressRankListSql = '('.join(',', $aList).')'; - } - - public function setAllowedTypesSQLList($sSql) - { - $this->sAllowedTypesSQLList = $sSql; - } - - public function setLanguagePreference($aLangPrefOrder) - { - $this->aLangPrefOrderSql = $this->oDB->getArraySQL( - $this->oDB->getDBQuotedList($aLangPrefOrder) - ); - } - - private function addressImportanceSql($sGeometry, $sPlaceId) - { - if ($this->sAnchorSql) { - $sSQL = 'ST_Distance('.$this->sAnchorSql.','.$sGeometry.')'; - } else { - $sSQL = '(SELECT max(ai_p.importance * (ai_p.rank_address + 2))'; - $sSQL .= ' FROM place_addressline ai_s, placex ai_p'; - $sSQL .= ' WHERE ai_s.place_id = '.$sPlaceId; - $sSQL .= ' AND ai_p.place_id = ai_s.address_place_id '; - $sSQL .= ' AND ai_s.isaddress '; - $sSQL .= ' AND ai_p.importance is not null)'; - } - - return $sSQL.' AS addressimportance,'; - } - - private function langAddressSql($sHousenumber) - { - if ($this->bAddressDetails) { - return ''; // langaddress will be computed from address details - } - - return 'get_address_by_language(place_id,'.$sHousenumber.','.$this->aLangPrefOrderSql.') AS langaddress,'; - } - - public function lookupOSMID($sType, $iID) - { - $sSQL = 'select place_id from placex where osm_type = :type and osm_id = :id'; - $iPlaceID = $this->oDB->getOne($sSQL, array(':type' => $sType, ':id' => $iID)); - - if (!$iPlaceID) { - return null; - } - - $aResults = $this->lookup(array($iPlaceID => new Result($iPlaceID)), 0, 30, true); - - return empty($aResults) ? null : reset($aResults); - } - - public function lookup($aResults, $iMinRank = 0, $iMaxRank = 30, $bAllowLinked = false) - { - Debug::newFunction('Place lookup'); - - if (empty($aResults)) { - return array(); - } - $aSubSelects = array(); - - $sPlaceIDs = Result::joinIdsByTable($aResults, Result::TABLE_PLACEX); - if ($sPlaceIDs) { - Debug::printVar('Ids from placex', $sPlaceIDs); - $sSQL = 'SELECT '; - $sSQL .= ' osm_type,'; - $sSQL .= ' osm_id,'; - $sSQL .= ' class,'; - $sSQL .= ' type,'; - $sSQL .= ' admin_level,'; - $sSQL .= ' rank_search,'; - $sSQL .= ' rank_address,'; - $sSQL .= ' min(place_id) AS place_id,'; - $sSQL .= ' min(parent_place_id) AS parent_place_id,'; - $sSQL .= ' -1 as housenumber,'; - $sSQL .= ' country_code,'; - $sSQL .= $this->langAddressSql('-1'); - $sSQL .= ' get_name_by_language(name,'.$this->aLangPrefOrderSql.') AS placename,'; - $sSQL .= " get_name_by_language(name, ARRAY['ref']) AS ref,"; - if ($this->bExtraTags) { - $sSQL .= 'hstore_to_json(extratags)::text AS extra,'; - } - if ($this->bNameDetails) { - $sSQL .= 'hstore_to_json(name)::text AS names,'; - } - $sSQL .= ' avg(ST_X(centroid)) AS lon, '; - $sSQL .= ' avg(ST_Y(centroid)) AS lat, '; - $sSQL .= ' COALESCE(importance,0.75-(rank_search::float/40)) AS importance, '; - $sSQL .= $this->addressImportanceSql( - 'ST_Collect(centroid)', - 'min(CASE WHEN placex.rank_search < 28 THEN placex.place_id ELSE placex.parent_place_id END)' - ); - $sSQL .= " COALESCE(extratags->'place', extratags->'linked_place') AS extra_place "; - $sSQL .= ' FROM placex'; - $sSQL .= " WHERE place_id in ($sPlaceIDs) "; - $sSQL .= ' AND ('; - $sSQL .= " placex.rank_address between $iMinRank and $iMaxRank "; - if (14 >= $iMinRank && 14 <= $iMaxRank) { - $sSQL .= " OR (extratags->'place') = 'city'"; - } - if ($this->sAddressRankListSql) { - $sSQL .= ' OR placex.rank_address in '.$this->sAddressRankListSql; - } - $sSQL .= ' ) '; - if ($this->sAllowedTypesSQLList) { - $sSQL .= 'AND placex.class in '.$this->sAllowedTypesSQLList; - } - if (!$bAllowLinked) { - $sSQL .= ' AND linked_place_id is null '; - } - $sSQL .= ' GROUP BY '; - $sSQL .= ' osm_type, '; - $sSQL .= ' osm_id, '; - $sSQL .= ' class, '; - $sSQL .= ' type, '; - $sSQL .= ' admin_level, '; - $sSQL .= ' rank_search, '; - $sSQL .= ' rank_address, '; - $sSQL .= ' housenumber,'; - $sSQL .= ' country_code, '; - $sSQL .= ' importance, '; - if (!$this->bDeDupe) { - $sSQL .= 'place_id,'; - } - if (!$this->bAddressDetails) { - $sSQL .= 'langaddress, '; - } - $sSQL .= ' placename, '; - $sSQL .= ' ref, '; - if ($this->bExtraTags) { - $sSQL .= 'extratags, '; - } - if ($this->bNameDetails) { - $sSQL .= 'name, '; - } - $sSQL .= ' extra_place '; - - $aSubSelects[] = $sSQL; - } - - // postcode table - $sPlaceIDs = Result::joinIdsByTable($aResults, Result::TABLE_POSTCODE); - if ($sPlaceIDs) { - Debug::printVar('Ids from location_postcode', $sPlaceIDs); - $sSQL = 'SELECT'; - $sSQL .= " 'P' as osm_type,"; - $sSQL .= ' (SELECT osm_id from placex p WHERE p.place_id = lp.parent_place_id) as osm_id,'; - $sSQL .= " 'place' as class, 'postcode' as type,"; - $sSQL .= ' null::smallint as admin_level, rank_search, rank_address,'; - $sSQL .= ' place_id, parent_place_id,'; - $sSQL .= ' -1 as housenumber,'; - $sSQL .= ' country_code,'; - $sSQL .= $this->langAddressSql('-1'); - $sSQL .= ' postcode as placename,'; - $sSQL .= ' postcode as ref,'; - if ($this->bExtraTags) { - $sSQL .= 'null::text AS extra,'; - } - if ($this->bNameDetails) { - $sSQL .= 'null::text AS names,'; - } - $sSQL .= ' ST_x(geometry) AS lon, ST_y(geometry) AS lat,'; - $sSQL .= ' (0.75-(rank_search::float/40)) AS importance, '; - $sSQL .= $this->addressImportanceSql('geometry', 'lp.parent_place_id'); - $sSQL .= ' null::text AS extra_place '; - $sSQL .= 'FROM location_postcode lp'; - $sSQL .= " WHERE place_id in ($sPlaceIDs) "; - $sSQL .= " AND lp.rank_address between $iMinRank and $iMaxRank"; - - $aSubSelects[] = $sSQL; - } - - // All other tables are rank 30 only. - if ($iMaxRank == 30) { - // TIGER table - if (CONST_Use_US_Tiger_Data) { - $sPlaceIDs = Result::joinIdsByTable($aResults, Result::TABLE_TIGER); - if ($sPlaceIDs) { - Debug::printVar('Ids from Tiger table', $sPlaceIDs); - $sHousenumbers = Result::sqlHouseNumberTable($aResults, Result::TABLE_TIGER); - // Tiger search only if a housenumber was searched and if it was found - // (realized through a join) - $sSQL = ' SELECT '; - $sSQL .= " 'T' AS osm_type, "; - $sSQL .= ' (SELECT osm_id from placex p WHERE p.place_id=blub.parent_place_id) as osm_id, '; - $sSQL .= " 'place' AS class, "; - $sSQL .= " 'house' AS type, "; - $sSQL .= ' null::smallint AS admin_level, '; - $sSQL .= ' 30 AS rank_search, '; - $sSQL .= ' 30 AS rank_address, '; - $sSQL .= ' place_id, '; - $sSQL .= ' parent_place_id, '; - $sSQL .= ' housenumber_for_place as housenumber,'; - $sSQL .= " 'us' AS country_code, "; - $sSQL .= $this->langAddressSql('housenumber_for_place'); - $sSQL .= ' null::text AS placename, '; - $sSQL .= ' null::text AS ref, '; - if ($this->bExtraTags) { - $sSQL .= 'null::text AS extra,'; - } - if ($this->bNameDetails) { - $sSQL .= 'null::text AS names,'; - } - $sSQL .= ' st_x(centroid) AS lon, '; - $sSQL .= ' st_y(centroid) AS lat,'; - $sSQL .= ' -1.15 AS importance, '; - $sSQL .= $this->addressImportanceSql('centroid', 'blub.parent_place_id'); - $sSQL .= ' null::text AS extra_place '; - $sSQL .= ' FROM ('; - $sSQL .= ' SELECT place_id, '; // interpolate the Tiger housenumbers here - $sSQL .= ' CASE WHEN startnumber != endnumber'; - $sSQL .= ' THEN ST_LineInterpolatePoint(linegeo, (housenumber_for_place-startnumber::float)/(endnumber-startnumber)::float)'; - $sSQL .= ' ELSE ST_LineInterpolatePoint(linegeo, 0.5) END AS centroid, '; - $sSQL .= ' parent_place_id, '; - $sSQL .= ' housenumber_for_place'; - $sSQL .= ' FROM ('; - $sSQL .= ' location_property_tiger '; - $sSQL .= ' JOIN (values '.$sHousenumbers.') AS housenumbers(place_id, housenumber_for_place) USING(place_id)) '; - $sSQL .= ' WHERE '; - $sSQL .= ' housenumber_for_place >= startnumber'; - $sSQL .= ' AND housenumber_for_place <= endnumber'; - $sSQL .= ' ) AS blub'; //postgres wants an alias here - - $aSubSelects[] = $sSQL; - } - } - - // osmline - interpolated housenumbers - $sPlaceIDs = Result::joinIdsByTable($aResults, Result::TABLE_OSMLINE); - if ($sPlaceIDs) { - Debug::printVar('Ids from interpolation', $sPlaceIDs); - $sHousenumbers = Result::sqlHouseNumberTable($aResults, Result::TABLE_OSMLINE); - // interpolation line search only if a housenumber was searched - // (realized through a join) - $sSQL = 'SELECT '; - $sSQL .= " 'W' AS osm_type, "; - $sSQL .= ' osm_id, '; - $sSQL .= " 'place' AS class, "; - $sSQL .= " 'house' AS type, "; - $sSQL .= ' null::smallint AS admin_level, '; - $sSQL .= ' 30 AS rank_search, '; - $sSQL .= ' 30 AS rank_address, '; - $sSQL .= ' place_id, '; - $sSQL .= ' parent_place_id, '; - $sSQL .= ' housenumber_for_place as housenumber,'; - $sSQL .= ' country_code, '; - $sSQL .= $this->langAddressSql('housenumber_for_place'); - $sSQL .= ' null::text AS placename, '; - $sSQL .= ' null::text AS ref, '; - if ($this->bExtraTags) { - $sSQL .= 'null::text AS extra, '; - } - if ($this->bNameDetails) { - $sSQL .= 'null::text AS names, '; - } - $sSQL .= ' st_x(centroid) AS lon, '; - $sSQL .= ' st_y(centroid) AS lat, '; - // slightly smaller than the importance for normal houses - $sSQL .= ' -0.1 AS importance, '; - $sSQL .= $this->addressImportanceSql('centroid', 'blub.parent_place_id'); - $sSQL .= ' null::text AS extra_place '; - $sSQL .= ' FROM ('; - $sSQL .= ' SELECT '; - $sSQL .= ' osm_id, '; - $sSQL .= ' place_id, '; - $sSQL .= ' country_code, '; - $sSQL .= ' CASE '; // interpolate the housenumbers here - $sSQL .= ' WHEN startnumber != endnumber '; - $sSQL .= ' THEN ST_LineInterpolatePoint(linegeo, (housenumber_for_place-startnumber::float)/(endnumber-startnumber)::float) '; - $sSQL .= ' ELSE linegeo '; - $sSQL .= ' END as centroid, '; - $sSQL .= ' parent_place_id, '; - $sSQL .= ' housenumber_for_place '; - $sSQL .= ' FROM ('; - $sSQL .= ' location_property_osmline '; - $sSQL .= ' JOIN (values '.$sHousenumbers.') AS housenumbers(place_id, housenumber_for_place) USING(place_id)'; - $sSQL .= ' ) '; - $sSQL .= ' WHERE housenumber_for_place >= 0 '; - $sSQL .= ' ) as blub'; //postgres wants an alias here - - $aSubSelects[] = $sSQL; - } - } - - if (empty($aSubSelects)) { - return array(); - } - - $sSQL = join(' UNION ', $aSubSelects); - Debug::printSQL($sSQL); - $aPlaces = $this->oDB->getAll($sSQL, null, 'Could not lookup place'); - - foreach ($aPlaces as &$aPlace) { - $aPlace['importance'] = (float) $aPlace['importance']; - if ($this->bAddressDetails) { - // to get addressdetails for tiger data, the housenumber is needed - $aPlace['address'] = new AddressDetails( - $this->oDB, - $aPlace['place_id'], - $aPlace['housenumber'], - $this->aLangPrefOrderSql - ); - $aPlace['langaddress'] = $aPlace['address']->getLocaleAddress(); - } - - if ($this->bExtraTags) { - if ($aPlace['extra']) { - $aPlace['sExtraTags'] = json_decode($aPlace['extra'], true); - } else { - $aPlace['sExtraTags'] = (object) array(); - } - } - - if ($this->bNameDetails) { - $aPlace['sNameDetails'] = $this->extractNames($aPlace['names']); - } - - $aPlace['addresstype'] = ClassTypes\getLabelTag( - $aPlace, - $aPlace['country_code'] - ); - - $aResults[$aPlace['place_id']] = $aPlace; - } - - $aResults = array_filter( - $aResults, - function ($v) { - return !($v instanceof Result); - } - ); - - Debug::printVar('Places', $aResults); - - return $aResults; - } - - - private function extractNames($sNames) - { - if (!$sNames) { - return (object) array(); - } - - $aFullNames = json_decode($sNames, true); - $aNames = array(); - - foreach ($aFullNames as $sKey => $sValue) { - if (strpos($sKey, '_place_') === 0) { - $sSubKey = substr($sKey, 7); - if (array_key_exists($sSubKey, $aFullNames)) { - $aNames[$sKey] = $sValue; - } else { - $aNames[$sSubKey] = $sValue; - } - } else { - $aNames[$sKey] = $sValue; - } - } - - return $aNames; - } - - - /* returns an array which will contain the keys - * aBoundingBox - * and may also contain one or more of the keys - * asgeojson - * askml - * assvg - * astext - * lat - * lon - */ - public function getOutlines($iPlaceID, $fLon = null, $fLat = null, $fRadius = null, $fLonReverse = null, $fLatReverse = null) - { - - $aOutlineResult = array(); - if (!$iPlaceID) { - return $aOutlineResult; - } - - // Get the bounding box and outline polygon - $sSQL = 'select place_id,0 as numfeatures,st_area(geometry) as area,'; - $sSQL .= ' ST_Y(centroid) as centrelat, ST_X(centroid) as centrelon,'; - $sSQL .= ' ST_YMin(geometry) as minlat,ST_YMax(geometry) as maxlat,'; - $sSQL .= ' ST_XMin(geometry) as minlon,ST_XMax(geometry) as maxlon'; - if ($this->bIncludePolygonAsGeoJSON) { - $sSQL .= ',ST_AsGeoJSON(geometry) as asgeojson'; - } - if ($this->bIncludePolygonAsKML) { - $sSQL .= ',ST_AsKML(geometry) as askml'; - } - if ($this->bIncludePolygonAsSVG) { - $sSQL .= ',ST_AsSVG(geometry) as assvg'; - } - if ($this->bIncludePolygonAsText) { - $sSQL .= ',ST_AsText(geometry) as astext'; - } - - $sSQL .= ' FROM (SELECT place_id'; - if ($fLonReverse != null && $fLatReverse != null) { - $sSQL .= ',CASE WHEN (class = \'highway\') AND (ST_GeometryType(geometry) = \'ST_LineString\') THEN '; - $sSQL .=' ST_ClosestPoint(geometry, ST_SetSRID(ST_Point('.$fLatReverse.','.$fLonReverse.'),4326))'; - $sSQL .=' ELSE centroid END AS centroid'; - } else { - $sSQL .= ',centroid'; - } - if ($this->fPolygonSimplificationThreshold > 0) { - $sSQL .= ',ST_SimplifyPreserveTopology(geometry,'.$this->fPolygonSimplificationThreshold.') as geometry'; - } else { - $sSQL .= ',geometry'; - } - $sSQL .= ' FROM placex where place_id = '.$iPlaceID.') as plx'; - - $aPointPolygon = $this->oDB->getRow($sSQL, null, 'Could not get outline'); - - if ($aPointPolygon && $aPointPolygon['place_id']) { - if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null) { - $aOutlineResult['lat'] = $aPointPolygon['centrelat']; - $aOutlineResult['lon'] = $aPointPolygon['centrelon']; - } - - if ($this->bIncludePolygonAsGeoJSON) { - $aOutlineResult['asgeojson'] = $aPointPolygon['asgeojson']; - } - if ($this->bIncludePolygonAsKML) { - $aOutlineResult['askml'] = $aPointPolygon['askml']; - } - if ($this->bIncludePolygonAsSVG) { - $aOutlineResult['assvg'] = $aPointPolygon['assvg']; - } - if ($this->bIncludePolygonAsText) { - $aOutlineResult['astext'] = $aPointPolygon['astext']; - } - - if (abs($aPointPolygon['minlat'] - $aPointPolygon['maxlat']) < 0.0000001) { - $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius; - $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius; - } - - if (abs($aPointPolygon['minlon'] - $aPointPolygon['maxlon']) < 0.0000001) { - $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius; - $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius; - } - - $aOutlineResult['aBoundingBox'] = array( - (string)$aPointPolygon['minlat'], - (string)$aPointPolygon['maxlat'], - (string)$aPointPolygon['minlon'], - (string)$aPointPolygon['maxlon'] - ); - } - - // as a fallback we generate a bounding box without knowing the size of the geometry - if ((!isset($aOutlineResult['aBoundingBox'])) && isset($fLon)) { - $aBounds = array( - 'minlat' => $fLat - $fRadius, - 'maxlat' => $fLat + $fRadius, - 'minlon' => $fLon - $fRadius, - 'maxlon' => $fLon + $fRadius - ); - - $aOutlineResult['aBoundingBox'] = array( - (string)$aBounds['minlat'], - (string)$aBounds['maxlat'], - (string)$aBounds['minlon'], - (string)$aBounds['maxlon'] - ); - } - return $aOutlineResult; - } -} diff --git a/lib-php/Result.php b/lib-php/Result.php deleted file mode 100644 index 4b244d1d..00000000 --- a/lib-php/Result.php +++ /dev/null @@ -1,129 +0,0 @@ - $this->iTable, - 'ID' => $this->iId, - 'House number' => $this->iHouseNumber, - 'Exact Matches' => $this->iExactMatches, - 'Result rank' => $this->iResultRank - ); - } - - - public function __construct($sId, $iTable = Result::TABLE_PLACEX) - { - $this->iTable = $iTable; - $this->iId = (int) $sId; - } - - public static function joinIdsByTable($aResults, $iTable) - { - return join(',', array_keys(array_filter( - $aResults, - function ($aValue) use ($iTable) { - return $aValue->iTable == $iTable; - } - ))); - } - - public static function joinIdsByTableMinRank($aResults, $iTable, $iMinAddressRank) - { - return join(',', array_keys(array_filter( - $aResults, - function ($aValue) use ($iTable, $iMinAddressRank) { - return $aValue->iTable == $iTable && $aValue->iAddressRank >= $iMinAddressRank; - } - ))); - } - - public static function joinIdsByTableMaxRank($aResults, $iTable, $iMaxAddressRank) - { - return join(',', array_keys(array_filter( - $aResults, - function ($aValue) use ($iTable, $iMaxAddressRank) { - return $aValue->iTable == $iTable && $aValue->iAddressRank <= $iMaxAddressRank; - } - ))); - } - - public static function sqlHouseNumberTable($aResults, $iTable) - { - $sHousenumbers = ''; - $sSep = ''; - foreach ($aResults as $oResult) { - if ($oResult->iTable == $iTable) { - $sHousenumbers .= $sSep.'('.$oResult->iId.','; - $sHousenumbers .= $oResult->iHouseNumber.')'; - $sSep = ','; - } - } - - return $sHousenumbers; - } - - /** - * Split a result array into highest ranked result and the rest - * - * @param object[] $aResults List of results to split. - * - * @return array[] - */ - public static function splitResults($aResults) - { - $aHead = array(); - $aTail = array(); - $iMinRank = 10000; - - foreach ($aResults as $oRes) { - if ($oRes->iResultRank < $iMinRank) { - $aTail += $aHead; - $aHead = array($oRes->iId => $oRes); - $iMinRank = $oRes->iResultRank; - } elseif ($oRes->iResultRank == $iMinRank) { - $aHead[$oRes->iId] = $oRes; - } else { - $aTail[$oRes->iId] = $oRes; - } - } - - return array('head' => $aHead, 'tail' => $aTail); - } -} diff --git a/lib-php/ReverseGeocode.php b/lib-php/ReverseGeocode.php deleted file mode 100644 index f6ea590f..00000000 --- a/lib-php/ReverseGeocode.php +++ /dev/null @@ -1,401 +0,0 @@ -oDB =& $oDB; - } - - - public function setZoom($iZoom) - { - // Zoom to rank, this could probably be calculated but a lookup gives fine control - $aZoomRank = array( - 0 => 2, // Continent / Sea - 1 => 2, - 2 => 2, - 3 => 4, // Country - 4 => 4, - 5 => 8, // State - 6 => 10, // Region - 7 => 10, - 8 => 12, // County - 9 => 12, - 10 => 17, // City - 11 => 17, - 12 => 18, // Town - 13 => 19, // Village - 14 => 22, // Neighbourhood - 15 => 25, // Locality - 16 => 26, // major street - 17 => 27, // minor street - 18 => 30, // or >, Building - 19 => 30, // or >, Building - ); - $this->iMaxRank = (isset($iZoom) && isset($aZoomRank[$iZoom]))?$aZoomRank[$iZoom]:28; - } - - /** - * Find the closest interpolation with the given search diameter. - * - * @param string $sPointSQL Reverse geocoding point as SQL - * @param float $fSearchDiam Search diameter - * - * @return Record of the interpolation or null. - */ - protected function lookupInterpolation($sPointSQL, $fSearchDiam) - { - Debug::newFunction('lookupInterpolation'); - $sSQL = 'SELECT place_id, parent_place_id, 30 as rank_search,'; - $sSQL .= ' (CASE WHEN endnumber != startnumber'; - $sSQL .= ' THEN (endnumber - startnumber) * ST_LineLocatePoint(linegeo,'.$sPointSQL.')'; - $sSQL .= ' ELSE startnumber END) as fhnr,'; - $sSQL .= ' startnumber, endnumber, step,'; - $sSQL .= ' ST_Distance(linegeo,'.$sPointSQL.') as distance'; - $sSQL .= ' FROM location_property_osmline'; - $sSQL .= ' WHERE ST_DWithin('.$sPointSQL.', linegeo, '.$fSearchDiam.')'; - $sSQL .= ' and indexed_status = 0 and startnumber is not NULL '; - $sSQL .= ' and parent_place_id != 0'; - $sSQL .= ' ORDER BY distance ASC limit 1'; - Debug::printSQL($sSQL); - - return $this->oDB->getRow( - $sSQL, - null, - 'Could not determine closest housenumber on an osm interpolation line.' - ); - } - - protected function lookupLargeArea($sPointSQL, $iMaxRank) - { - $sCountryCode = $this->getCountryCode($sPointSQL); - if (CONST_Search_WithinCountries and $sCountryCode == null) { - return null; - } - - if ($iMaxRank > 4) { - $aPlace = $this->lookupPolygon($sPointSQL, $iMaxRank); - if ($aPlace) { - return new Result($aPlace['place_id']); - } - } - - // If no polygon which contains the searchpoint is found, - // searches in the country_osm_grid table for a polygon. - return $this->lookupInCountry($sPointSQL, $iMaxRank, $sCountryCode); - } - - protected function getCountryCode($sPointSQL) - { - Debug::newFunction('getCountryCode'); - // searches for polygon in table country_osm_grid which contains the searchpoint - // and searches for the nearest place node to the searchpoint in this polygon - $sSQL = 'SELECT country_code FROM country_osm_grid'; - $sSQL .= ' WHERE ST_CONTAINS(geometry, '.$sPointSQL.') LIMIT 1'; - Debug::printSQL($sSQL); - - $sCountryCode = $this->oDB->getOne( - $sSQL, - null, - 'Could not determine country polygon containing the point.' - ); - return $sCountryCode; - } - - protected function lookupInCountry($sPointSQL, $iMaxRank, $sCountryCode) - { - Debug::newFunction('lookupInCountry'); - if ($sCountryCode) { - if ($iMaxRank > 4) { - // look for place nodes with the given country code - $sSQL = 'SELECT place_id FROM'; - $sSQL .= ' (SELECT place_id, rank_search,'; - $sSQL .= ' ST_distance('.$sPointSQL.', geometry) as distance'; - $sSQL .= ' FROM placex'; - $sSQL .= ' WHERE osm_type = \'N\''; - $sSQL .= ' AND country_code = \''.$sCountryCode.'\''; - $sSQL .= ' AND rank_address between 4 and 25'; // needed to select right index - $sSQL .= ' AND rank_search between 5 and ' .min(25, $iMaxRank); - $sSQL .= ' AND type != \'postcode\''; - $sSQL .= ' AND name IS NOT NULL '; - $sSQL .= ' and indexed_status = 0 and linked_place_id is null'; - $sSQL .= ' AND ST_Buffer(geometry, reverse_place_diameter(rank_search)) && '.$sPointSQL; - $sSQL .= ') as a '; - $sSQL .= 'WHERE distance <= reverse_place_diameter(rank_search)'; - $sSQL .= ' ORDER BY rank_search DESC, distance ASC'; - $sSQL .= ' LIMIT 1'; - Debug::printSQL($sSQL); - - $aPlace = $this->oDB->getRow($sSQL, null, 'Could not determine place node.'); - Debug::printVar('Country node', $aPlace); - - if ($aPlace) { - return new Result($aPlace['place_id']); - } - } - - // still nothing, then return the country object - $sSQL = 'SELECT place_id, ST_distance('.$sPointSQL.', centroid) as distance'; - $sSQL .= ' FROM placex'; - $sSQL .= ' WHERE country_code = \''.$sCountryCode.'\''; - $sSQL .= ' AND rank_search = 4 AND rank_address = 4'; - $sSQL .= ' AND class in (\'boundary\', \'place\')'; - $sSQL .= ' AND linked_place_id is null'; - $sSQL .= ' ORDER BY distance ASC'; - Debug::printSQL($sSQL); - - $aPlace = $this->oDB->getRow($sSQL, null, 'Could not determine place node.'); - Debug::printVar('Country place', $aPlace); - if ($aPlace) { - return new Result($aPlace['place_id']); - } - } - - return null; - } - - /** - * Search for areas or nodes for areas or nodes between state and suburb level. - * - * @param string $sPointSQL Search point as SQL string. - * @param int $iMaxRank Maximum address rank of the feature. - * - * @return Record of the found feature or null. - * - * Searches first for polygon that contains the search point. - * If such a polygon is found, place nodes with a higher rank are - * searched inside the polygon. - */ - protected function lookupPolygon($sPointSQL, $iMaxRank) - { - Debug::newFunction('lookupPolygon'); - // polygon search begins at suburb-level - if ($iMaxRank > 25) { - $iMaxRank = 25; - } - // no polygon search over country-level - if ($iMaxRank < 5) { - $iMaxRank = 5; - } - // search for polygon - $sSQL = 'SELECT place_id, parent_place_id, rank_address, rank_search FROM'; - $sSQL .= '(select place_id, parent_place_id, rank_address, rank_search, country_code, geometry'; - $sSQL .= ' FROM placex'; - $sSQL .= ' WHERE ST_GeometryType(geometry) in (\'ST_Polygon\', \'ST_MultiPolygon\')'; - // Ensure that query planner doesn't use the index on rank_search. - $sSQL .= ' AND coalesce(rank_search, 0) between 5 and ' .$iMaxRank; - $sSQL .= ' AND rank_address between 4 and 25'; // needed for index selection - $sSQL .= ' AND geometry && '.$sPointSQL; - $sSQL .= ' AND type != \'postcode\' '; - $sSQL .= ' AND name is not null'; - $sSQL .= ' AND indexed_status = 0 and linked_place_id is null'; - $sSQL .= ' ORDER BY rank_search DESC LIMIT 50 ) as a'; - $sSQL .= ' WHERE ST_Contains(geometry, '.$sPointSQL.' )'; - $sSQL .= ' ORDER BY rank_search DESC LIMIT 1'; - Debug::printSQL($sSQL); - - $aPoly = $this->oDB->getRow($sSQL, null, 'Could not determine polygon containing the point.'); - Debug::printVar('Polygon result', $aPoly); - - if ($aPoly) { - // if a polygon is found, search for placenodes begins ... - $iRankAddress = $aPoly['rank_address']; - $iRankSearch = $aPoly['rank_search']; - $iPlaceID = $aPoly['place_id']; - - if ($iRankSearch != $iMaxRank) { - $sSQL = 'SELECT place_id FROM '; - $sSQL .= '(SELECT place_id, rank_search, country_code, geometry,'; - $sSQL .= ' ST_distance('.$sPointSQL.', geometry) as distance'; - $sSQL .= ' FROM placex'; - $sSQL .= ' WHERE osm_type = \'N\''; - $sSQL .= ' AND rank_search > '.$iRankSearch; - $sSQL .= ' AND rank_search <= '.$iMaxRank; - $sSQL .= ' AND rank_address between 4 and 25'; // needed to select right index - $sSQL .= ' AND type != \'postcode\''; - $sSQL .= ' AND name IS NOT NULL '; - $sSQL .= ' AND indexed_status = 0 AND linked_place_id is null'; - $sSQL .= ' AND ST_Buffer(geometry, reverse_place_diameter(rank_search)) && '.$sPointSQL; - $sSQL .= ' ORDER BY rank_search DESC, distance ASC'; - $sSQL .= ' limit 100) as a'; - $sSQL .= ' WHERE ST_Contains((SELECT geometry FROM placex WHERE place_id = '.$iPlaceID.'), geometry )'; - $sSQL .= ' AND distance <= reverse_place_diameter(rank_search)'; - $sSQL .= ' ORDER BY rank_search DESC, distance ASC'; - $sSQL .= ' LIMIT 1'; - Debug::printSQL($sSQL); - - $aPlaceNode = $this->oDB->getRow($sSQL, null, 'Could not determine place node.'); - Debug::printVar('Nearest place node', $aPlaceNode); - if ($aPlaceNode) { - return $aPlaceNode; - } - } - } - return $aPoly; - } - - - public function lookup($fLat, $fLon, $bDoInterpolation = true) - { - return $this->lookupPoint( - 'ST_SetSRID(ST_Point('.$fLon.','.$fLat.'),4326)', - $bDoInterpolation - ); - } - - public function lookupPoint($sPointSQL, $bDoInterpolation = true) - { - Debug::newFunction('lookupPoint'); - // Find the nearest point - $fSearchDiam = 0.006; - $oResult = null; - $aPlace = null; - - // for POI or street level - if ($this->iMaxRank >= 26) { - // starts if the search is on POI or street level, - // searches for the nearest POI or street, - // if a street is found and a POI is searched for, - // the nearest POI which the found street is a parent of is chosen. - $sSQL = 'select place_id,parent_place_id,rank_address,country_code,'; - $sSQL .= ' ST_distance('.$sPointSQL.', geometry) as distance'; - $sSQL .= ' FROM '; - $sSQL .= ' placex'; - $sSQL .= ' WHERE ST_DWithin('.$sPointSQL.', geometry, '.$fSearchDiam.')'; - $sSQL .= ' AND'; - $sSQL .= ' rank_address between 26 and '.$this->iMaxRank; - $sSQL .= ' and (name is not null or housenumber is not null'; - $sSQL .= ' or rank_address between 26 and 27)'; - $sSQL .= ' and (rank_address between 26 and 27'; - $sSQL .= ' or ST_GeometryType(geometry) != \'ST_LineString\')'; - $sSQL .= ' and class not in (\'boundary\')'; - $sSQL .= ' and indexed_status = 0 and linked_place_id is null'; - $sSQL .= ' and (ST_GeometryType(geometry) not in (\'ST_Polygon\',\'ST_MultiPolygon\') '; - $sSQL .= ' OR ST_DWithin('.$sPointSQL.', centroid, '.$fSearchDiam.'))'; - $sSQL .= ' ORDER BY distance ASC limit 1'; - Debug::printSQL($sSQL); - - $aPlace = $this->oDB->getRow($sSQL, null, 'Could not determine closest place.'); - - Debug::printVar('POI/street level result', $aPlace); - if ($aPlace) { - $iPlaceID = $aPlace['place_id']; - $oResult = new Result($iPlaceID); - $iRankAddress = $aPlace['rank_address']; - } - - if ($aPlace) { - // if street and maxrank > streetlevel - if ($iRankAddress <= 27 && $this->iMaxRank > 27) { - // find the closest object (up to a certain radius) of which the street is a parent of - $sSQL = ' select place_id,'; - $sSQL .= ' ST_distance('.$sPointSQL.', geometry) as distance'; - $sSQL .= ' FROM '; - $sSQL .= ' placex'; - // radius ? - $sSQL .= ' WHERE ST_DWithin('.$sPointSQL.', geometry, 0.001)'; - $sSQL .= ' AND parent_place_id = '.$iPlaceID; - $sSQL .= ' and rank_address > 28'; - $sSQL .= ' and ST_GeometryType(geometry) != \'ST_LineString\''; - $sSQL .= ' and (name is not null or housenumber is not null)'; - $sSQL .= ' and class not in (\'boundary\')'; - $sSQL .= ' and indexed_status = 0 and linked_place_id is null'; - $sSQL .= ' ORDER BY distance ASC limit 1'; - Debug::printSQL($sSQL); - - $aStreet = $this->oDB->getRow($sSQL, null, 'Could not determine closest place.'); - Debug::printVar('Closest POI result', $aStreet); - - if ($aStreet) { - $aPlace = $aStreet; - $oResult = new Result($aStreet['place_id']); - $iRankAddress = 30; - } - } - - // In the US we can check TIGER data for nearest housenumber - if (CONST_Use_US_Tiger_Data - && $iRankAddress <= 27 - && $aPlace['country_code'] == 'us' - && $this->iMaxRank >= 28 - ) { - $sSQL = 'SELECT place_id,parent_place_id,30 as rank_search,'; - $sSQL .= ' (endnumber - startnumber) * ST_LineLocatePoint(linegeo,'.$sPointSQL.') as fhnr,'; - $sSQL .= ' startnumber, endnumber, step,'; - $sSQL .= ' ST_Distance('.$sPointSQL.', linegeo) as distance'; - $sSQL .= ' FROM location_property_tiger WHERE parent_place_id = '.$oResult->iId; - $sSQL .= ' AND ST_DWithin('.$sPointSQL.', linegeo, 0.001)'; - $sSQL .= ' ORDER BY distance ASC limit 1'; - Debug::printSQL($sSQL); - - $aPlaceTiger = $this->oDB->getRow($sSQL, null, 'Could not determine closest Tiger place.'); - Debug::printVar('Tiger house number result', $aPlaceTiger); - - if ($aPlaceTiger) { - $aPlace = $aPlaceTiger; - $oResult = new Result($aPlaceTiger['place_id'], Result::TABLE_TIGER); - $iRndNum = max(0, round($aPlaceTiger['fhnr'] / $aPlaceTiger['step']) * $aPlaceTiger['step']); - $oResult->iHouseNumber = $aPlaceTiger['startnumber'] + $iRndNum; - if ($oResult->iHouseNumber > $aPlaceTiger['endnumber']) { - $oResult->iHouseNumber = $aPlaceTiger['endnumber']; - } - $iRankAddress = 30; - } - } - } - - if ($bDoInterpolation && $this->iMaxRank >= 30) { - $fDistance = $fSearchDiam; - if ($aPlace) { - // We can't reliably go from the closest street to an - // interpolation line because the closest interpolation - // may have a different street segments as a parent. - // Therefore allow an interpolation line to take precedence - // even when the street is closer. - $fDistance = $iRankAddress < 28 ? 0.001 : $aPlace['distance']; - } - - $aHouse = $this->lookupInterpolation($sPointSQL, $fDistance); - Debug::printVar('Interpolation result', $aPlace); - - if ($aHouse) { - $oResult = new Result($aHouse['place_id'], Result::TABLE_OSMLINE); - $iRndNum = max(0, round($aHouse['fhnr'] / $aHouse['step']) * $aHouse['step']); - $oResult->iHouseNumber = $aHouse['startnumber'] + $iRndNum; - if ($oResult->iHouseNumber > $aHouse['endnumber']) { - $oResult->iHouseNumber = $aHouse['endnumber']; - } - $aPlace = $aHouse; - } - } - - if (!$aPlace) { - // if no POI or street is found ... - $oResult = $this->lookupLargeArea($sPointSQL, 25); - } - } else { - // lower than street level ($iMaxRank < 26 ) - $oResult = $this->lookupLargeArea($sPointSQL, $this->iMaxRank); - } - - Debug::printVar('Final result', $oResult); - return $oResult; - } -} diff --git a/lib-php/SearchContext.php b/lib-php/SearchContext.php deleted file mode 100644 index 3223b5c9..00000000 --- a/lib-php/SearchContext.php +++ /dev/null @@ -1,319 +0,0 @@ -aFullNameWords = $aWordList; - } - - public function getFullNameTerms() - { - return $this->aFullNameWords; - } - - /** - * Check if a reference point is defined. - * - * @return bool True if a reference point is defined. - */ - public function hasNearPoint() - { - return $this->fNearRadius !== false; - } - - /** - * Get radius around reference point. - * - * @return float Search radius around reference point. - */ - public function nearRadius() - { - return $this->fNearRadius; - } - - /** - * Set search reference point in WGS84. - * - * If set, then only places around this point will be taken into account. - * - * @param float $fLat Latitude of point. - * @param float $fLon Longitude of point. - * @param float $fRadius Search radius around point. - * - * @return void - */ - public function setNearPoint($fLat, $fLon, $fRadius = 0.1) - { - $this->fNearRadius = $fRadius; - $this->sqlNear = 'ST_SetSRID(ST_Point('.$fLon.','.$fLat.'),4326)'; - } - - /** - * Check if the search is geographically restricted. - * - * Searches are restricted if a reference point is given or if - * a bounded viewbox is set. - * - * @return bool True, if the search is geographically bounded. - */ - public function isBoundedSearch() - { - return $this->hasNearPoint() || ($this->sqlViewboxSmall && $this->bViewboxBounded); - } - - /** - * Set rectangular viewbox. - * - * The viewbox may be bounded which means that no search results - * must be outside the viewbox. - * - * @param float[4] $aViewBox Coordinates of the viewbox. - * @param bool $bBounded True if the viewbox is bounded. - * - * @return void - */ - public function setViewboxFromBox(&$aViewBox, $bBounded) - { - $this->bViewboxBounded = $bBounded; - $this->sqlViewboxCentre = ''; - - $this->sqlViewboxSmall = sprintf( - 'ST_SetSRID(ST_MakeBox2D(ST_Point(%F,%F),ST_Point(%F,%F)),4326)', - $aViewBox[0], - $aViewBox[1], - $aViewBox[2], - $aViewBox[3] - ); - - $fHeight = abs($aViewBox[0] - $aViewBox[2]); - $fWidth = abs($aViewBox[1] - $aViewBox[3]); - - $this->sqlViewboxLarge = sprintf( - 'ST_SetSRID(ST_MakeBox2D(ST_Point(%F,%F),ST_Point(%F,%F)),4326)', - max($aViewBox[0], $aViewBox[2]) + $fHeight, - max($aViewBox[1], $aViewBox[3]) + $fWidth, - min($aViewBox[0], $aViewBox[2]) - $fHeight, - min($aViewBox[1], $aViewBox[3]) - $fWidth - ); - } - - /** - * Set viewbox along a route. - * - * The viewbox may be bounded which means that no search results - * must be outside the viewbox. - * - * @param object $oDB Nominatim::DB instance to use for computing the box. - * @param string[] $aRoutePoints List of x,y coordinates along a route. - * @param float $fRouteWidth Buffer around the route to use. - * @param bool $bBounded True if the viewbox bounded. - * - * @return void - */ - public function setViewboxFromRoute(&$oDB, $aRoutePoints, $fRouteWidth, $bBounded) - { - $this->bViewboxBounded = $bBounded; - $this->sqlViewboxCentre = "ST_SetSRID('LINESTRING("; - $sSep = ''; - foreach ($aRoutePoints as $aPoint) { - $fPoint = (float)$aPoint; - $this->sqlViewboxCentre .= $sSep.$fPoint; - $sSep = ($sSep == ' ') ? ',' : ' '; - } - $this->sqlViewboxCentre .= ")'::geometry,4326)"; - - $sSQL = 'ST_BUFFER('.$this->sqlViewboxCentre.','.($fRouteWidth/69).')'; - $sGeom = $oDB->getOne('select '.$sSQL, null, 'Could not get small viewbox'); - $this->sqlViewboxSmall = "'".$sGeom."'::geometry"; - - $sSQL = 'ST_BUFFER('.$this->sqlViewboxCentre.','.($fRouteWidth/30).')'; - $sGeom = $oDB->getOne('select '.$sSQL, null, 'Could not get large viewbox'); - $this->sqlViewboxLarge = "'".$sGeom."'::geometry"; - } - - /** - * Set list of excluded place IDs. - * - * @param integer[] $aExcluded List of IDs. - * - * @return void - */ - public function setExcludeList($aExcluded) - { - $this->sqlExcludeList = ' not in ('.join(',', $aExcluded).')'; - } - - /** - * Set list of countries to restrict search to. - * - * @param string[] $aCountries List of two-letter lower-case country codes. - * - * @return void - */ - public function setCountryList($aCountries) - { - $this->sqlCountryList = '('.join(',', array_map('addQuotes', $aCountries)).')'; - $this->aCountryList = $aCountries; - } - - /** - * Extract a reference point from a query string. - * - * @param string $sQuery Query to scan. - * - * @return string The remaining query string. - */ - public function setNearPointFromQuery($sQuery) - { - $aResult = parseLatLon($sQuery); - - if ($aResult !== false - && $aResult[1] <= 90.1 - && $aResult[1] >= -90.1 - && $aResult[2] <= 180.1 - && $aResult[2] >= -180.1 - ) { - $this->setNearPoint($aResult[1], $aResult[2]); - $sQuery = trim(str_replace($aResult[0], ' ', $sQuery)); - } - - return $sQuery; - } - - /** - * Get an SQL snippet for computing the distance from the reference point. - * - * @param string $sObj SQL variable name to compute the distance from. - * - * @return string An SQL string. - */ - public function distanceSQL($sObj) - { - return 'ST_Distance('.$this->sqlNear.", $sObj)"; - } - - /** - * Get an SQL snippet for checking if something is within range of the - * reference point. - * - * @param string $sObj SQL variable name to compute if it is within range. - * - * @return string An SQL string. - */ - public function withinSQL($sObj) - { - return sprintf('ST_DWithin(%s, %s, %F)', $sObj, $this->sqlNear, $this->fNearRadius); - } - - /** - * Get an SQL snippet of the importance factor of the viewbox. - * - * The importance factor is computed by checking if an object is within - * the viewbox and/or the extended version of the viewbox. - * - * @param string $sObj SQL variable name of object to weight the importance - * - * @return string SQL snippet of the factor with a leading multiply sign. - */ - public function viewboxImportanceSQL($sObj) - { - $sSQL = ''; - - if ($this->sqlViewboxSmall) { - $sSQL = " * CASE WHEN ST_Contains($this->sqlViewboxSmall, $sObj) THEN 1 ELSE 0.5 END"; - } - if ($this->sqlViewboxLarge) { - $sSQL = " * CASE WHEN ST_Contains($this->sqlViewboxLarge, $sObj) THEN 1 ELSE 0.5 END"; - } - - return $sSQL; - } - - /** - * SQL snippet checking if a place ID should be excluded. - * - * @param string $sVariable SQL variable name of place ID to check, - * potentially prefixed with more SQL. - * - * @return string SQL snippet. - */ - public function excludeSQL($sVariable) - { - if ($this->sqlExcludeList) { - return $sVariable.$this->sqlExcludeList; - } - - return ''; - } - - /** - * Check if the given country is covered by the search context. - * - * @param string $sCountryCode Country code of the country to check. - * - * @return True, if no country code restrictions are set or the - * country is included in the country list. - */ - public function isCountryApplicable($sCountryCode) - { - return $this->aCountryList === null || in_array($sCountryCode, $this->aCountryList); - } - - public function debugInfo() - { - return array( - 'Near radius' => $this->fNearRadius, - 'Near point (SQL)' => $this->sqlNear, - 'Bounded viewbox' => $this->bViewboxBounded, - 'Viewbox (SQL, small)' => $this->sqlViewboxSmall, - 'Viewbox (SQL, large)' => $this->sqlViewboxLarge, - 'Viewbox (SQL, centre)' => $this->sqlViewboxCentre, - 'Countries (SQL)' => $this->sqlCountryList, - 'Excluded IDs (SQL)' => $this->sqlExcludeList - ); - } -} diff --git a/lib-php/SearchDescription.php b/lib-php/SearchDescription.php deleted file mode 100644 index 5d2caf00..00000000 --- a/lib-php/SearchDescription.php +++ /dev/null @@ -1,985 +0,0 @@ -oContext = $oContext; - } - - /** - * Get current search rank. - * - * The higher the search rank the lower the likelihood that the - * search is a correct interpretation of the search query. - * - * @return integer Search rank. - */ - public function getRank() - { - return $this->iSearchRank; - } - - /** - * Extract key/value pairs from a query. - * - * Key/value pairs are recognised if they are of the form [=]. - * If multiple terms of this kind are found then all terms are removed - * but only the first is used for search. - * - * @param string $sQuery Original query string. - * - * @return string The query string with the special search patterns removed. - */ - public function extractKeyValuePairs($sQuery) - { - // Search for terms of kind [=]. - preg_match_all( - '/\\[([\\w_]*)=([\\w_]*)\\]/', - $sQuery, - $aSpecialTermsRaw, - PREG_SET_ORDER - ); - - foreach ($aSpecialTermsRaw as $aTerm) { - $sQuery = str_replace($aTerm[0], ' ', $sQuery); - if (!$this->hasOperator()) { - $this->setPoiSearch(Operator::TYPE, $aTerm[1], $aTerm[2]); - } - } - - return $sQuery; - } - - /** - * Check if the combination of parameters is sensible. - * - * @return bool True, if the search looks valid. - */ - public function isValidSearch() - { - if (empty($this->aName)) { - if ($this->sHouseNumber) { - return false; - } - if (!$this->sClass && !$this->sCountryCode) { - return false; - } - } - if ($this->bNameNeedsAddress && empty($this->aAddress)) { - return false; - } - - return true; - } - - /////////// Search building functions - - /** - * Create a copy of this search description adding to search rank. - * - * @param integer $iTermCost Cost to add to the current search rank. - * - * @return object Cloned search description. - */ - public function clone($iTermCost) - { - $oSearch = clone $this; - $oSearch->iSearchRank += $iTermCost; - - return $oSearch; - } - - /** - * Check if the search currently includes a name. - * - * @param bool bIncludeNonNames If true stop-word tokens are taken into - * account, too. - * - * @return bool True, if search has a name. - */ - public function hasName($bIncludeNonNames = false) - { - return !empty($this->aName) - || (!empty($this->aNameNonSearch) && $bIncludeNonNames); - } - - /** - * Check if the search currently includes an address term. - * - * @return bool True, if any address term is included, including stop-word - * terms. - */ - public function hasAddress() - { - return !empty($this->aAddress) || !empty($this->aAddressNonSearch); - } - - /** - * Check if a country restriction is currently included in the search. - * - * @return bool True, if a country restriction is set. - */ - public function hasCountry() - { - return $this->sCountryCode !== ''; - } - - /** - * Check if a postcode is currently included in the search. - * - * @return bool True, if a postcode is set. - */ - public function hasPostcode() - { - return $this->sPostcode !== ''; - } - - /** - * Check if a house number is set for the search. - * - * @return bool True, if a house number is set. - */ - public function hasHousenumber() - { - return $this->sHouseNumber !== ''; - } - - /** - * Check if a special type of place is requested. - * - * param integer iOperator When set, check for the particular - * operator used for the special type. - * - * @return bool True, if speial type is requested or, if requested, - * a special type with the given operator. - */ - public function hasOperator($iOperator = null) - { - return $iOperator === null ? $this->iOperator != Operator::NONE : $this->iOperator == $iOperator; - } - - /** - * Add the given token to the list of terms to search for in the address. - * - * @param integer iID ID of term to add. - * @param bool bSearchable Term should be used to search for result - * (i.e. term is not a stop word). - */ - public function addAddressToken($iId, $bSearchable = true) - { - if ($bSearchable) { - $this->aAddress[$iId] = $iId; - } else { - $this->aAddressNonSearch[$iId] = $iId; - } - } - - /** - * Add the given full-word token to the list of terms to search for in the - * name. - * - * @param integer iId ID of term to add. - * @param bool bRareName True if the term is infrequent enough to not - * require other constraints for efficient search. - */ - public function addNameToken($iId, $bRareName) - { - $this->aName[$iId] = $iId; - $this->bRareName = $bRareName; - $this->bNameNeedsAddress = false; - } - - /** - * Add the given partial token to the list of terms to search for in - * the name. - * - * @param integer iID ID of term to add. - * @param bool bSearchable Term should be used to search for result - * (i.e. term is not a stop word). - * @param bool bNeedsAddress True if the term is too unspecific to be used - * in a stand-alone search without an address - * to narrow down the search. - * @param integer iPhraseNumber Index of phrase, where the partial term - * appears. - */ - public function addPartialNameToken($iId, $bSearchable, $bNeedsAddress, $iPhraseNumber) - { - if (empty($this->aName)) { - $this->bNameNeedsAddress = $bNeedsAddress; - } elseif ($bSearchable && count($this->aName) >= 2) { - $this->bNameNeedsAddress = false; - } else { - $this->bNameNeedsAddress &= $bNeedsAddress; - } - if ($bSearchable) { - $this->aName[$iId] = $iId; - } else { - $this->aNameNonSearch[$iId] = $iId; - } - $this->iNamePhrase = $iPhraseNumber; - } - - /** - * Set country restriction for the search. - * - * @param string sCountryCode Country code of country to restrict search to. - */ - public function setCountry($sCountryCode) - { - $this->sCountryCode = $sCountryCode; - $this->iNamePhrase = -1; - } - - /** - * Set postcode search constraint. - * - * @param string sPostcode Postcode the result should have. - */ - public function setPostcode($sPostcode) - { - $this->sPostcode = $sPostcode; - $this->iNamePhrase = -1; - } - - /** - * Make this search a search for a postcode object. - * - * @param integer iId Token Id for the postcode. - * @param string sPostcode Postcode to look for. - */ - public function setPostcodeAsName($iId, $sPostcode) - { - $this->iOperator = Operator::POSTCODE; - $this->aAddress = array_merge($this->aAddress, $this->aName); - $this->aName = array($iId => $sPostcode); - $this->bRareName = true; - $this->iNamePhrase = -1; - } - - /** - * Set house number search cnstraint. - * - * @param string sNumber House number the result should have. - */ - public function setHousenumber($sNumber) - { - $this->sHouseNumber = $sNumber; - $this->iNamePhrase = -1; - } - - /** - * Make this search a search for a house number. - * - * @param integer iId Token Id for the house number. - */ - public function setHousenumberAsName($iId) - { - $this->aAddress = array_merge($this->aAddress, $this->aName); - $this->bRareName = false; - $this->bNameNeedsAddress = true; - $this->aName = array($iId => $iId); - $this->iNamePhrase = -1; - } - - /** - * Make this search a POI search. - * - * In a POI search, objects are not (only) searched by their name - * but also by the primary OSM key/value pair (class and type in Nominatim). - * - * @param integer $iOperator Type of POI search - * @param string $sClass Class (or OSM tag key) of POI. - * @param string $sType Type (or OSM tag value) of POI. - * - * @return void - */ - public function setPoiSearch($iOperator, $sClass, $sType) - { - $this->iOperator = $iOperator; - $this->sClass = $sClass; - $this->sType = $sType; - $this->iNamePhrase = -1; - } - - public function getNamePhrase() - { - return $this->iNamePhrase; - } - - /** - * Get the global search context. - * - * @return object Objects of global search constraints. - */ - public function getContext() - { - return $this->oContext; - } - - /////////// Query functions - - - /** - * Query database for places that match this search. - * - * @param object $oDB Nominatim::DB instance to use. - * @param integer $iMinRank Minimum address rank to restrict search to. - * @param integer $iMaxRank Maximum address rank to restrict search to. - * @param integer $iLimit Maximum number of results. - * - * @return mixed[] An array with two fields: IDs contains the list of - * matching place IDs and houseNumber the houseNumber - * if applicable or -1 if not. - */ - public function query(&$oDB, $iMinRank, $iMaxRank, $iLimit) - { - $aResults = array(); - - if ($this->sCountryCode - && empty($this->aName) - && !$this->iOperator - && !$this->sClass - && !$this->oContext->hasNearPoint() - ) { - // Just looking for a country - look it up - if (4 >= $iMinRank && 4 <= $iMaxRank) { - $aResults = $this->queryCountry($oDB); - } - } elseif (empty($this->aName) && empty($this->aAddress)) { - // Neither name nor address? Then we must be - // looking for a POI in a geographic area. - if ($this->oContext->isBoundedSearch()) { - $aResults = $this->queryNearbyPoi($oDB, $iLimit); - } - } elseif ($this->iOperator == Operator::POSTCODE) { - // looking for postcode - $aResults = $this->queryPostcode($oDB, $iLimit); - } else { - // Ordinary search: - // First search for places according to name and address. - $aResults = $this->queryNamedPlace( - $oDB, - $iMinRank, - $iMaxRank, - $iLimit - ); - - // finally get POIs if requested - if ($this->sClass && !empty($aResults)) { - $aResults = $this->queryPoiByOperator($oDB, $aResults, $iLimit); - } - } - - Debug::printDebugTable('Place IDs', $aResults); - - if (!empty($aResults) && $this->sPostcode) { - $sPlaceIds = Result::joinIdsByTable($aResults, Result::TABLE_PLACEX); - if ($sPlaceIds) { - $sSQL = 'SELECT place_id FROM placex'; - $sSQL .= ' WHERE place_id in ('.$sPlaceIds.')'; - $sSQL .= " AND postcode != '".$this->sPostcode."'"; - Debug::printSQL($sSQL); - $aFilteredPlaceIDs = $oDB->getCol($sSQL); - if ($aFilteredPlaceIDs) { - foreach ($aFilteredPlaceIDs as $iPlaceId) { - $aResults[$iPlaceId]->iResultRank++; - } - } - } - } - - return $aResults; - } - - - private function queryCountry(&$oDB) - { - $sSQL = 'SELECT place_id FROM placex '; - $sSQL .= "WHERE country_code='".$this->sCountryCode."'"; - $sSQL .= ' AND rank_search = 4'; - if ($this->oContext->bViewboxBounded) { - $sSQL .= ' AND ST_Intersects('.$this->oContext->sqlViewboxSmall.', geometry)'; - } - $sSQL .= ' ORDER BY st_area(geometry) DESC LIMIT 1'; - - Debug::printSQL($sSQL); - - $iPlaceId = $oDB->getOne($sSQL); - - $aResults = array(); - if ($iPlaceId) { - $aResults[$iPlaceId] = new Result($iPlaceId); - } - - return $aResults; - } - - private function queryNearbyPoi(&$oDB, $iLimit) - { - if (!$this->sClass) { - return array(); - } - - $aDBResults = array(); - $sPoiTable = $this->poiTable(); - - if ($oDB->tableExists($sPoiTable)) { - $sSQL = 'SELECT place_id FROM '.$sPoiTable.' ct'; - if ($this->oContext->sqlCountryList) { - $sSQL .= ' JOIN placex USING (place_id)'; - } - if ($this->oContext->hasNearPoint()) { - $sSQL .= ' WHERE '.$this->oContext->withinSQL('ct.centroid'); - } elseif ($this->oContext->bViewboxBounded) { - $sSQL .= ' WHERE ST_Contains('.$this->oContext->sqlViewboxSmall.', ct.centroid)'; - } - if ($this->oContext->sqlCountryList) { - $sSQL .= ' AND country_code in '.$this->oContext->sqlCountryList; - } - $sSQL .= $this->oContext->excludeSQL(' AND place_id'); - if ($this->oContext->sqlViewboxCentre) { - $sSQL .= ' ORDER BY ST_Distance('; - $sSQL .= $this->oContext->sqlViewboxCentre.', ct.centroid) ASC'; - } elseif ($this->oContext->hasNearPoint()) { - $sSQL .= ' ORDER BY '.$this->oContext->distanceSQL('ct.centroid').' ASC'; - } - $sSQL .= " LIMIT $iLimit"; - Debug::printSQL($sSQL); - $aDBResults = $oDB->getCol($sSQL); - } - - if ($this->oContext->hasNearPoint()) { - $sSQL = 'SELECT place_id FROM placex WHERE '; - $sSQL .= 'class = :class and type = :type'; - $sSQL .= ' AND '.$this->oContext->withinSQL('geometry'); - $sSQL .= ' AND linked_place_id is null'; - if ($this->oContext->sqlCountryList) { - $sSQL .= ' AND country_code in '.$this->oContext->sqlCountryList; - } - $sSQL .= ' ORDER BY '.$this->oContext->distanceSQL('centroid').' ASC'; - $sSQL .= " LIMIT $iLimit"; - Debug::printSQL($sSQL); - $aDBResults = $oDB->getCol( - $sSQL, - array(':class' => $this->sClass, ':type' => $this->sType) - ); - } - - $aResults = array(); - foreach ($aDBResults as $iPlaceId) { - $aResults[$iPlaceId] = new Result($iPlaceId); - } - - return $aResults; - } - - private function queryPostcode(&$oDB, $iLimit) - { - $sSQL = 'SELECT p.place_id FROM location_postcode p '; - - if (!empty($this->aAddress)) { - $sSQL .= ', search_name s '; - $sSQL .= 'WHERE s.place_id = p.parent_place_id '; - $sSQL .= 'AND array_cat(s.nameaddress_vector, s.name_vector)'; - $sSQL .= ' @> '.$oDB->getArraySQL($this->aAddress).' AND '; - } else { - $sSQL .= 'WHERE '; - } - - $sSQL .= "p.postcode = '".reset($this->aName)."'"; - $sSQL .= $this->countryCodeSQL(' AND p.country_code'); - if ($this->oContext->bViewboxBounded) { - $sSQL .= ' AND ST_Intersects('.$this->oContext->sqlViewboxSmall.', geometry)'; - } - $sSQL .= $this->oContext->excludeSQL(' AND p.place_id'); - $sSQL .= " LIMIT $iLimit"; - - Debug::printSQL($sSQL); - - $aResults = array(); - foreach ($oDB->getCol($sSQL) as $iPlaceId) { - $aResults[$iPlaceId] = new Result($iPlaceId, Result::TABLE_POSTCODE); - } - - return $aResults; - } - - private function queryNamedPlace(&$oDB, $iMinAddressRank, $iMaxAddressRank, $iLimit) - { - $aTerms = array(); - $aOrder = array(); - - if (!empty($this->aName)) { - $aTerms[] = 'name_vector @> '.$oDB->getArraySQL($this->aName); - } - if (!empty($this->aAddress)) { - // For infrequent name terms disable index usage for address - if ($this->bRareName) { - $aTerms[] = 'array_cat(nameaddress_vector,ARRAY[]::integer[]) @> '.$oDB->getArraySQL($this->aAddress); - } else { - $aTerms[] = 'nameaddress_vector @> '.$oDB->getArraySQL($this->aAddress); - } - } - - $sCountryTerm = $this->countryCodeSQL('country_code'); - if ($sCountryTerm) { - $aTerms[] = $sCountryTerm; - } - - if ($this->sHouseNumber) { - $aTerms[] = 'address_rank between 16 and 30'; - } elseif (!$this->sClass || $this->iOperator == Operator::NAME) { - if ($iMinAddressRank > 0) { - $aTerms[] = "((address_rank between $iMinAddressRank and $iMaxAddressRank) or (search_rank between $iMinAddressRank and $iMaxAddressRank))"; - } - } - - if ($this->oContext->hasNearPoint()) { - $aTerms[] = $this->oContext->withinSQL('centroid'); - $aOrder[] = $this->oContext->distanceSQL('centroid'); - } elseif ($this->sPostcode) { - if (empty($this->aAddress)) { - $aTerms[] = "EXISTS(SELECT place_id FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."' AND ST_DWithin(search_name.centroid, p.geometry, 0.12))"; - } else { - $aOrder[] = "(SELECT min(ST_Distance(search_name.centroid, p.geometry)) FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."')"; - } - } - - $sExcludeSQL = $this->oContext->excludeSQL('place_id'); - if ($sExcludeSQL) { - $aTerms[] = $sExcludeSQL; - } - - if ($this->oContext->bViewboxBounded) { - $aTerms[] = 'centroid && '.$this->oContext->sqlViewboxSmall; - } - - if ($this->sHouseNumber) { - $sImportanceSQL = '- abs(26 - address_rank) + 3'; - } else { - $sImportanceSQL = '(CASE WHEN importance = 0 OR importance IS NULL THEN 0.75001-(search_rank::float/40) ELSE importance END)'; - } - $sImportanceSQL .= $this->oContext->viewboxImportanceSQL('centroid'); - $aOrder[] = "$sImportanceSQL DESC"; - - $aFullNameAddress = $this->oContext->getFullNameTerms(); - if (!empty($aFullNameAddress)) { - $sExactMatchSQL = ' ( '; - $sExactMatchSQL .= ' SELECT count(*) FROM ( '; - $sExactMatchSQL .= ' SELECT unnest('.$oDB->getArraySQL($aFullNameAddress).')'; - $sExactMatchSQL .= ' INTERSECT '; - $sExactMatchSQL .= ' SELECT unnest(nameaddress_vector)'; - $sExactMatchSQL .= ' ) s'; - $sExactMatchSQL .= ') as exactmatch'; - $aOrder[] = 'exactmatch DESC'; - } else { - $sExactMatchSQL = '0::int as exactmatch'; - } - - if (empty($aTerms)) { - return array(); - } - - if ($this->hasHousenumber()) { - $sHouseNumberRegex = $oDB->getDBQuoted('\\\\m'.$this->sHouseNumber.'\\\\M'); - - // Housenumbers on streets and places. - $sPlacexSql = 'SELECT array_agg(place_id) FROM placex'; - $sPlacexSql .= ' WHERE parent_place_id = sin.place_id AND sin.address_rank < 30'; - $sPlacexSql .= $this->oContext->excludeSQL(' AND place_id'); - $sPlacexSql .= ' and housenumber ~* E'.$sHouseNumberRegex; - - // Interpolations on streets and places. - $sInterpolSql = 'null'; - $sTigerSql = 'null'; - if (preg_match('/^[0-9]+$/', $this->sHouseNumber)) { - $sIpolHnr = 'WHERE parent_place_id = sin.place_id '; - $sIpolHnr .= ' AND startnumber is not NULL AND sin.address_rank < 30'; - $sIpolHnr .= ' AND '.$this->sHouseNumber.' between startnumber and endnumber'; - $sIpolHnr .= ' AND ('.$this->sHouseNumber.' - startnumber) % step = 0'; - - $sInterpolSql = 'SELECT array_agg(place_id) FROM location_property_osmline '.$sIpolHnr; - if (CONST_Use_US_Tiger_Data) { - $sTigerSql = 'SELECT array_agg(place_id) FROM location_property_tiger '.$sIpolHnr; - $sTigerSql .= " and sin.country_code = 'us'"; - } - } - - if ($this->sClass) { - $iLimit = 40; - } - - $sSelfHnr = 'SELECT * FROM placex WHERE place_id = search_name.place_id'; - $sSelfHnr .= ' AND housenumber ~* E'.$sHouseNumberRegex; - - $aTerms[] = '(address_rank < 30 or exists('.$sSelfHnr.'))'; - - - $sSQL = 'SELECT sin.*, '; - $sSQL .= '('.$sPlacexSql.') as placex_hnr, '; - $sSQL .= '('.$sInterpolSql.') as interpol_hnr, '; - $sSQL .= '('.$sTigerSql.') as tiger_hnr '; - $sSQL .= ' FROM ('; - $sSQL .= ' SELECT place_id, address_rank, country_code,'.$sExactMatchSQL.','; - $sSQL .= ' CASE WHEN importance = 0 OR importance IS NULL'; - $sSQL .= ' THEN 0.75001-(search_rank::float/40) ELSE importance END as importance'; - $sSQL .= ' FROM search_name'; - $sSQL .= ' WHERE '.join(' and ', $aTerms); - $sSQL .= ' ORDER BY '.join(', ', $aOrder); - $sSQL .= ' LIMIT 40000'; - $sSQL .= ') as sin'; - $sSQL .= ' ORDER BY address_rank = 30 desc, placex_hnr, interpol_hnr, tiger_hnr,'; - $sSQL .= ' importance'; - $sSQL .= ' LIMIT '.$iLimit; - } else { - if ($this->sClass) { - $iLimit = 40; - } - - $sSQL = 'SELECT place_id, address_rank, '.$sExactMatchSQL; - $sSQL .= ' FROM search_name'; - $sSQL .= ' WHERE '.join(' and ', $aTerms); - $sSQL .= ' ORDER BY '.join(', ', $aOrder); - $sSQL .= ' LIMIT '.$iLimit; - } - - Debug::printSQL($sSQL); - - $aDBResults = $oDB->getAll($sSQL, null, 'Could not get places for search terms.'); - - $aResults = array(); - - foreach ($aDBResults as $aResult) { - $oResult = new Result($aResult['place_id']); - $oResult->iExactMatches = $aResult['exactmatch']; - $oResult->iAddressRank = $aResult['address_rank']; - - $bNeedResult = true; - if ($this->hasHousenumber() && $aResult['address_rank'] < 30) { - if ($aResult['placex_hnr']) { - foreach (explode(',', substr($aResult['placex_hnr'], 1, -1)) as $sPlaceID) { - $iPlaceID = intval($sPlaceID); - $oHnrResult = new Result($iPlaceID); - $oHnrResult->iExactMatches = $aResult['exactmatch']; - $oHnrResult->iAddressRank = 30; - $aResults[$iPlaceID] = $oHnrResult; - $bNeedResult = false; - } - } - if ($aResult['interpol_hnr']) { - foreach (explode(',', substr($aResult['interpol_hnr'], 1, -1)) as $sPlaceID) { - $iPlaceID = intval($sPlaceID); - $oHnrResult = new Result($iPlaceID, Result::TABLE_OSMLINE); - $oHnrResult->iExactMatches = $aResult['exactmatch']; - $oHnrResult->iAddressRank = 30; - $oHnrResult->iHouseNumber = intval($this->sHouseNumber); - $aResults[$iPlaceID] = $oHnrResult; - $bNeedResult = false; - } - } - if ($aResult['tiger_hnr']) { - foreach (explode(',', substr($aResult['tiger_hnr'], 1, -1)) as $sPlaceID) { - $iPlaceID = intval($sPlaceID); - $oHnrResult = new Result($iPlaceID, Result::TABLE_TIGER); - $oHnrResult->iExactMatches = $aResult['exactmatch']; - $oHnrResult->iAddressRank = 30; - $oHnrResult->iHouseNumber = intval($this->sHouseNumber); - $aResults[$iPlaceID] = $oHnrResult; - $bNeedResult = false; - } - } - - if ($aResult['address_rank'] < 26) { - $oResult->iResultRank += 2; - } else { - $oResult->iResultRank++; - } - } - - if ($bNeedResult) { - $aResults[$aResult['place_id']] = $oResult; - } - } - - return $aResults; - } - - - private function queryPoiByOperator(&$oDB, $aParentIDs, $iLimit) - { - $aResults = array(); - $sPlaceIDs = Result::joinIdsByTable($aParentIDs, Result::TABLE_PLACEX); - - if (!$sPlaceIDs) { - return $aResults; - } - - if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NAME) { - // If they were searching for a named class (i.e. 'Kings Head pub') - // then we might have an extra match - $sSQL = 'SELECT place_id FROM placex '; - $sSQL .= " WHERE place_id in ($sPlaceIDs)"; - $sSQL .= " AND class='".$this->sClass."' "; - $sSQL .= " AND type='".$this->sType."'"; - $sSQL .= ' AND linked_place_id is null'; - $sSQL .= $this->oContext->excludeSQL(' AND place_id'); - $sSQL .= ' ORDER BY rank_search ASC '; - $sSQL .= " LIMIT $iLimit"; - - Debug::printSQL($sSQL); - - foreach ($oDB->getCol($sSQL) as $iPlaceId) { - $aResults[$iPlaceId] = new Result($iPlaceId); - } - } - - // NEAR and IN are handled the same - if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NEAR) { - $sClassTable = $this->poiTable(); - $bCacheTable = $oDB->tableExists($sClassTable); - - $sSQL = "SELECT min(rank_search) FROM placex WHERE place_id in ($sPlaceIDs)"; - Debug::printSQL($sSQL); - $iMaxRank = (int) $oDB->getOne($sSQL); - - // For state / country level searches the normal radius search doesn't work very well - $sPlaceGeom = false; - if ($iMaxRank < 9 && $bCacheTable) { - // Try and get a polygon to search in instead - $sSQL = 'SELECT geometry FROM placex'; - $sSQL .= " WHERE place_id in ($sPlaceIDs)"; - $sSQL .= " AND rank_search < $iMaxRank + 5"; - $sSQL .= ' AND ST_Area(Box2d(geometry)) < 20'; - $sSQL .= " AND ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon')"; - $sSQL .= ' ORDER BY rank_search ASC '; - $sSQL .= ' LIMIT 1'; - Debug::printSQL($sSQL); - $sPlaceGeom = $oDB->getOne($sSQL); - } - - if ($sPlaceGeom) { - $sPlaceIDs = false; - } else { - $iMaxRank += 5; - $sSQL = 'SELECT place_id FROM placex'; - $sSQL .= " WHERE place_id in ($sPlaceIDs) and rank_search < $iMaxRank"; - Debug::printSQL($sSQL); - $aPlaceIDs = $oDB->getCol($sSQL); - $sPlaceIDs = join(',', $aPlaceIDs); - } - - if ($sPlaceIDs || $sPlaceGeom) { - $fRange = 0.01; - if ($bCacheTable) { - // More efficient - can make the range bigger - $fRange = 0.05; - - $sOrderBySQL = ''; - if ($this->oContext->hasNearPoint()) { - $sOrderBySQL = $this->oContext->distanceSQL('l.centroid'); - } elseif ($sPlaceIDs) { - $sOrderBySQL = 'ST_Distance(l.centroid, f.geometry)'; - } elseif ($sPlaceGeom) { - $sOrderBySQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)"; - } - - $sSQL = 'SELECT distinct i.place_id'; - if ($sOrderBySQL) { - $sSQL .= ', i.order_term'; - } - $sSQL .= ' from (SELECT l.place_id'; - if ($sOrderBySQL) { - $sSQL .= ','.$sOrderBySQL.' as order_term'; - } - $sSQL .= ' from '.$sClassTable.' as l'; - - if ($sPlaceIDs) { - $sSQL .= ',placex as f WHERE '; - $sSQL .= "f.place_id in ($sPlaceIDs) "; - $sSQL .= " AND ST_DWithin(l.centroid, f.centroid, $fRange)"; - } elseif ($sPlaceGeom) { - $sSQL .= " WHERE ST_Contains('$sPlaceGeom', l.centroid)"; - } - - $sSQL .= $this->oContext->excludeSQL(' AND l.place_id'); - $sSQL .= 'limit 300) i '; - if ($sOrderBySQL) { - $sSQL .= 'order by order_term asc'; - } - $sSQL .= " limit $iLimit"; - - Debug::printSQL($sSQL); - - foreach ($oDB->getCol($sSQL) as $iPlaceId) { - $aResults[$iPlaceId] = new Result($iPlaceId); - } - } else { - if ($this->oContext->hasNearPoint()) { - $fRange = $this->oContext->nearRadius(); - } - - $sOrderBySQL = ''; - if ($this->oContext->hasNearPoint()) { - $sOrderBySQL = $this->oContext->distanceSQL('l.geometry'); - } else { - $sOrderBySQL = 'ST_Distance(l.geometry, f.geometry)'; - } - - $sSQL = 'SELECT distinct l.place_id'; - if ($sOrderBySQL) { - $sSQL .= ','.$sOrderBySQL.' as orderterm'; - } - $sSQL .= ' FROM placex as l, placex as f'; - $sSQL .= " WHERE f.place_id in ($sPlaceIDs)"; - $sSQL .= " AND ST_DWithin(l.geometry, f.centroid, $fRange)"; - $sSQL .= " AND l.class='".$this->sClass."'"; - $sSQL .= " AND l.type='".$this->sType."'"; - $sSQL .= $this->oContext->excludeSQL(' AND l.place_id'); - if ($sOrderBySQL) { - $sSQL .= 'ORDER BY orderterm ASC'; - } - $sSQL .= " limit $iLimit"; - - Debug::printSQL($sSQL); - - foreach ($oDB->getCol($sSQL) as $iPlaceId) { - $aResults[$iPlaceId] = new Result($iPlaceId); - } - } - } - } - - return $aResults; - } - - private function poiTable() - { - return 'place_classtype_'.$this->sClass.'_'.$this->sType; - } - - private function countryCodeSQL($sVar) - { - if ($this->sCountryCode) { - return $sVar.' = \''.$this->sCountryCode."'"; - } - if ($this->oContext->sqlCountryList) { - return $sVar.' in '.$this->oContext->sqlCountryList; - } - - return ''; - } - - /////////// Sort functions - - - public static function bySearchRank($a, $b) - { - if ($a->iSearchRank == $b->iSearchRank) { - return $a->iOperator + strlen($a->sHouseNumber) - - $b->iOperator - strlen($b->sHouseNumber); - } - - return $a->iSearchRank < $b->iSearchRank ? -1 : 1; - } - - //////////// Debugging functions - - - public function debugInfo() - { - return array( - 'Search rank' => $this->iSearchRank, - 'Country code' => $this->sCountryCode, - 'Name terms' => $this->aName, - 'Name terms (stop words)' => $this->aNameNonSearch, - 'Address terms' => $this->aAddress, - 'Address terms (stop words)' => $this->aAddressNonSearch, - 'Address terms (full words)' => $this->aFullNameAddress ?? '', - 'Special search' => $this->iOperator, - 'Class' => $this->sClass, - 'Type' => $this->sType, - 'House number' => $this->sHouseNumber, - 'Postcode' => $this->sPostcode - ); - } - - public function dumpAsHtmlTableRow(&$aWordIDs) - { - $kf = function ($k) use (&$aWordIDs) { - return $aWordIDs[$k] ?? '['.$k.']'; - }; - - echo ''; - echo "$this->iSearchRank"; - echo ''.join(', ', array_map($kf, $this->aName)).''; - echo ''.join(', ', array_map($kf, $this->aNameNonSearch)).''; - echo ''.join(', ', array_map($kf, $this->aAddress)).''; - echo ''.join(', ', array_map($kf, $this->aAddressNonSearch)).''; - echo ''.$this->sCountryCode.''; - echo ''.Operator::toString($this->iOperator).''; - echo ''.$this->sClass.''; - echo ''.$this->sType.''; - echo ''.$this->sPostcode.''; - echo ''.$this->sHouseNumber.''; - - echo ''; - } -} diff --git a/lib-php/SearchPosition.php b/lib-php/SearchPosition.php deleted file mode 100644 index aeeeb2c3..00000000 --- a/lib-php/SearchPosition.php +++ /dev/null @@ -1,95 +0,0 @@ -sPhraseType = $sPhraseType; - $this->iPhrase = $iPhrase; - $this->iNumPhrases = $iNumPhrases; - } - - public function setTokenPosition($iToken, $iNumTokens) - { - $this->iToken = $iToken; - $this->iNumTokens = $iNumTokens; - } - - /** - * Check if the phrase can be of the given type. - * - * @param string $sType Type of phrse requested. - * - * @return True if the phrase is untyped or of the given type. - */ - public function maybePhrase($sType) - { - return $this->sPhraseType == '' || $this->sPhraseType == $sType; - } - - /** - * Check if the phrase is exactly of the given type. - * - * @param string $sType Type of phrse requested. - * - * @return True if the phrase of the given type. - */ - public function isPhrase($sType) - { - return $this->sPhraseType == $sType; - } - - /** - * Return true if the token is the very first in the query. - */ - public function isFirstToken() - { - return $this->iPhrase == 0 && $this->iToken == 0; - } - - /** - * Check if the token is the final one in the query. - */ - public function isLastToken() - { - return $this->iToken + 1 == $this->iNumTokens && $this->iPhrase + 1 == $this->iNumPhrases; - } - - /** - * Check if the current token is part of the first phrase in the query. - */ - public function isFirstPhrase() - { - return $this->iPhrase == 0; - } - - /** - * Get the phrase position in the query. - */ - public function getPhrase() - { - return $this->iPhrase; - } -} diff --git a/lib-php/Shell.php b/lib-php/Shell.php deleted file mode 100644 index 4be13235..00000000 --- a/lib-php/Shell.php +++ /dev/null @@ -1,92 +0,0 @@ -baseCmd = $sBaseCmd; - $this->aParams = array(); - $this->aEnv = null; // null = use the same environment as the current PHP process - - $this->stdoutString = null; - - foreach ($aParams as $sParam) { - $this->addParams($sParam); - } - } - - public function addParams(...$aParams) - { - foreach ($aParams as $sParam) { - if (isset($sParam) && $sParam !== null && $sParam !== '') { - array_push($this->aParams, $sParam); - } - } - return $this; - } - - public function addEnvPair($sKey, $sVal) - { - if (isset($sKey) && $sKey && isset($sVal)) { - if (!isset($this->aEnv)) { - $this->aEnv = $_ENV; - } - $this->aEnv = array_merge($this->aEnv, array($sKey => $sVal), $_ENV); - } - return $this; - } - - public function escapedCmd() - { - $aEscaped = array_map(function ($sParam) { - return $this->escapeParam($sParam); - }, array_merge(array($this->baseCmd), $this->aParams)); - - return join(' ', $aEscaped); - } - - public function run($bExitOnFail = false) - { - $sCmd = $this->escapedCmd(); - // $aEnv does not need escaping, proc_open seems to handle it fine - - $aFDs = array( - 0 => array('pipe', 'r'), - 1 => STDOUT, - 2 => STDERR - ); - $aPipes = null; - $hProc = @proc_open($sCmd, $aFDs, $aPipes, null, $this->aEnv); - if (!is_resource($hProc)) { - throw new \Exception('Unable to run command: ' . $sCmd); - } - - fclose($aPipes[0]); // no stdin - - $iStat = proc_close($hProc); - - if ($iStat != 0 && $bExitOnFail) { - exit($iStat); - } - - return $iStat; - } - - private function escapeParam($sParam) - { - return (preg_match('/^-*\w+$/', $sParam)) ? $sParam : escapeshellarg($sParam); - } -} diff --git a/lib-php/SimpleWordList.php b/lib-php/SimpleWordList.php deleted file mode 100644 index 7009d370..00000000 --- a/lib-php/SimpleWordList.php +++ /dev/null @@ -1,144 +0,0 @@ - 0) { - $this->aWords = explode(' ', $sPhrase); - } else { - $this->aWords = array(); - } - } - - /** - * Get all possible tokens that are present in this word list. - * - * @return array The list of string tokens in the word list. - */ - public function getTokens() - { - $aTokens = array(); - $iNumWords = count($this->aWords); - - for ($i = 0; $i < $iNumWords; $i++) { - $sPhrase = $this->aWords[$i]; - $aTokens[$sPhrase] = $sPhrase; - - for ($j = $i + 1; $j < $iNumWords; $j++) { - $sPhrase .= ' '.$this->aWords[$j]; - $aTokens[$sPhrase] = $sPhrase; - } - } - - return $aTokens; - } - - /** - * Compute all possible permutations of phrase splits that result in - * words which are in the token list. - */ - public function getWordSets($oTokens) - { - $iNumWords = count($this->aWords); - - if ($iNumWords == 0) { - return null; - } - - // Caches the word set for the partial phrase up to word i. - $aSetCache = array_fill(0, $iNumWords, array()); - - // Initialise first element of cache. There can only be the word. - if ($oTokens->containsAny($this->aWords[0])) { - $aSetCache[0][] = array($this->aWords[0]); - } - - // Now do the next elements using what we already have. - for ($i = 1; $i < $iNumWords; $i++) { - for ($j = $i; $j > 0; $j--) { - $sPartial = $j == $i ? $this->aWords[$j] : $this->aWords[$j].' '.$sPartial; - if (!empty($aSetCache[$j - 1]) && $oTokens->containsAny($sPartial)) { - $aPartial = array($sPartial); - foreach ($aSetCache[$j - 1] as $aSet) { - if (count($aSet) < SimpleWordList::MAX_WORDSET_LEN) { - $aSetCache[$i][] = array_merge($aSet, $aPartial); - } - } - if (count($aSetCache[$i]) > 2 * SimpleWordList::MAX_WORDSETS) { - usort( - $aSetCache[$i], - array('\Nominatim\SimpleWordList', 'cmpByArraylen') - ); - $aSetCache[$i] = array_slice( - $aSetCache[$i], - 0, - SimpleWordList::MAX_WORDSETS - ); - } - } - } - - // finally the current full phrase - $sPartial = $this->aWords[0].' '.$sPartial; - if ($oTokens->containsAny($sPartial)) { - $aSetCache[$i][] = array($sPartial); - } - } - - $aWordSets = $aSetCache[$iNumWords - 1]; - usort($aWordSets, array('\Nominatim\SimpleWordList', 'cmpByArraylen')); - return array_slice($aWordSets, 0, SimpleWordList::MAX_WORDSETS); - } - - /** - * Custom search routine which takes two arrays. The array with the fewest - * items wins. If same number of items then the one with the longest first - * element wins. - */ - public static function cmpByArraylen($aA, $aB) - { - $iALen = count($aA); - $iBLen = count($aB); - - if ($iALen == $iBLen) { - return strlen($aB[0]) <=> strlen($aA[0]); - } - - return ($iALen < $iBLen) ? -1 : 1; - } - - public function debugInfo() - { - return $this->aWords; - } -} diff --git a/lib-php/SpecialSearchOperator.php b/lib-php/SpecialSearchOperator.php deleted file mode 100644 index 94df59ea..00000000 --- a/lib-php/SpecialSearchOperator.php +++ /dev/null @@ -1,52 +0,0 @@ -getConstants(); - - Operator::$aConstantNames = array(); - foreach ($aConstants as $sName => $iValue) { - Operator::$aConstantNames[$iValue] = $sName; - } - } - - return Operator::$aConstantNames[$iOperator]; - } -} diff --git a/lib-php/Status.php b/lib-php/Status.php deleted file mode 100644 index 4f1555cd..00000000 --- a/lib-php/Status.php +++ /dev/null @@ -1,59 +0,0 @@ -oDB =& $oDB; - } - - public function status() - { - if (!$this->oDB) { - throw new Exception('No database', 700); - } - - try { - $this->oDB->connect(); - } catch (\Nominatim\DatabaseError $e) { - throw new Exception('Database connection failed', 700); - } - - $oTokenizer = new \Nominatim\Tokenizer($this->oDB); - $oTokenizer->checkStatus(); - } - - public function dataDate() - { - $sSQL = 'SELECT EXTRACT(EPOCH FROM lastimportdate) FROM import_status LIMIT 1'; - $iDataDateEpoch = $this->oDB->getOne($sSQL); - - if ($iDataDateEpoch === false) { - throw new Exception('Import date is not available', 705); - } - - return $iDataDateEpoch; - } - - public function databaseVersion() - { - $sSQL = 'SELECT value FROM nominatim_properties WHERE property = \'database_version\''; - return $this->oDB->getOne($sSQL); - } -} diff --git a/lib-php/TokenCountry.php b/lib-php/TokenCountry.php deleted file mode 100644 index 3f93f45e..00000000 --- a/lib-php/TokenCountry.php +++ /dev/null @@ -1,82 +0,0 @@ -iId = $iId; - $this->sCountryCode = $sCountryCode; - } - - public function getId() - { - return $this->iId; - } - - /** - * Check if the token can be added to the given search. - * Derive new searches by adding this token to an existing search. - * - * @param object $oSearch Partial search description derived so far. - * @param object $oPosition Description of the token position within - the query. - * - * @return True if the token is compatible with the search configuration - * given the position. - */ - public function isExtendable($oSearch, $oPosition) - { - return !$oSearch->hasCountry() - && $oPosition->maybePhrase('country') - && $oSearch->getContext()->isCountryApplicable($this->sCountryCode); - } - - /** - * Derive new searches by adding this token to an existing search. - * - * @param object $oSearch Partial search description derived so far. - * @param object $oPosition Description of the token position within - the query. - * - * @return SearchDescription[] List of derived search descriptions. - */ - public function extendSearch($oSearch, $oPosition) - { - $oNewSearch = $oSearch->clone($oPosition->isLastToken() ? 1 : 6); - $oNewSearch->setCountry($this->sCountryCode); - - return array($oNewSearch); - } - - public function debugInfo() - { - return array( - 'ID' => $this->iId, - 'Type' => 'country', - 'Info' => $this->sCountryCode - ); - } - - public function debugCode() - { - return 'C'; - } -} diff --git a/lib-php/TokenHousenumber.php b/lib-php/TokenHousenumber.php deleted file mode 100644 index 62c2a624..00000000 --- a/lib-php/TokenHousenumber.php +++ /dev/null @@ -1,116 +0,0 @@ -iId = $iId; - $this->sToken = $sToken; - } - - public function getId() - { - return $this->iId; - } - - /** - * Check if the token can be added to the given search. - * Derive new searches by adding this token to an existing search. - * - * @param object $oSearch Partial search description derived so far. - * @param object $oPosition Description of the token position within - the query. - * - * @return True if the token is compatible with the search configuration - * given the position. - */ - public function isExtendable($oSearch, $oPosition) - { - return !$oSearch->hasHousenumber() - && !$oSearch->hasOperator(\Nominatim\Operator::POSTCODE) - && $oPosition->maybePhrase('street'); - } - - /** - * Derive new searches by adding this token to an existing search. - * - * @param object $oSearch Partial search description derived so far. - * @param object $oPosition Description of the token position within - the query. - * - * @return SearchDescription[] List of derived search descriptions. - */ - public function extendSearch($oSearch, $oPosition) - { - $aNewSearches = array(); - - // sanity check: if the housenumber is not mainly made - // up of numbers, add a penalty - $iSearchCost = 1; - if (preg_match('/\\d/', $this->sToken) === 0 - || preg_match_all('/[^0-9 ]/', $this->sToken, $aMatches) > 3) { - $iSearchCost += strlen($this->sToken) - 1; - } - if (!$oSearch->hasOperator(\Nominatim\Operator::NONE)) { - $iSearchCost++; - } - if (empty($this->iId)) { - $iSearchCost++; - } - // also must not appear in the middle of the address - if ($oSearch->hasAddress() || $oSearch->hasPostcode()) { - $iSearchCost++; - } - - $oNewSearch = $oSearch->clone($iSearchCost); - $oNewSearch->setHousenumber($this->sToken); - $aNewSearches[] = $oNewSearch; - - // Housenumbers may appear in the name when the place has its own - // address terms. - if ($this->iId !== null - && ($oSearch->getNamePhrase() >= 0 || !$oSearch->hasName()) - && !$oSearch->hasAddress() - ) { - $oNewSearch = $oSearch->clone($iSearchCost); - $oNewSearch->setHousenumberAsName($this->iId); - - $aNewSearches[] = $oNewSearch; - } - - return $aNewSearches; - } - - - public function debugInfo() - { - return array( - 'ID' => $this->iId, - 'Type' => 'house number', - 'Info' => array('nr' => $this->sToken) - ); - } - - public function debugCode() - { - return 'H'; - } -} diff --git a/lib-php/TokenList.php b/lib-php/TokenList.php deleted file mode 100644 index 9a3950a1..00000000 --- a/lib-php/TokenList.php +++ /dev/null @@ -1,134 +0,0 @@ -aTokens); - } - - /** - * Check if there are tokens for the given token word. - * - * @param string $sWord Token word to look for. - * - * @return bool True if there is one or more token for the token word. - */ - public function contains($sWord) - { - return isset($this->aTokens[$sWord]); - } - - /** - * Check if there are partial or full tokens for the given word. - * - * @param string $sWord Token word to look for. - * - * @return bool True if there is one or more token for the token word. - */ - public function containsAny($sWord) - { - return isset($this->aTokens[$sWord]); - } - - /** - * Get the list of tokens for the given token word. - * - * @param string $sWord Token word to look for. - * - * @return object[] Array of tokens for the given token word or an - * empty array if no tokens could be found. - */ - public function get($sWord) - { - return isset($this->aTokens[$sWord]) ? $this->aTokens[$sWord] : array(); - } - - public function getFullWordIDs() - { - $ids = array(); - - foreach ($this->aTokens as $aTokenList) { - foreach ($aTokenList as $oToken) { - if (is_a($oToken, '\Nominatim\Token\Word')) { - $ids[$oToken->getId()] = $oToken->getId(); - } - } - } - - return $ids; - } - - /** - * Add a new token for the given word. - * - * @param string $sWord Word the token describes. - * @param object $oToken Token object to add. - * - * @return void - */ - public function addToken($sWord, $oToken) - { - if (isset($this->aTokens[$sWord])) { - $this->aTokens[$sWord][] = $oToken; - } else { - $this->aTokens[$sWord] = array($oToken); - } - } - - public function debugTokenByWordIdList() - { - $aWordsIDs = array(); - foreach ($this->aTokens as $sToken => $aWords) { - foreach ($aWords as $aToken) { - $iId = $aToken->getId(); - if ($iId !== null) { - $aWordsIDs[$iId] = '#'.$sToken.'('.$aToken->debugCode().' '.$iId.')#'; - } - } - } - - return $aWordsIDs; - } - - public function debugInfo() - { - return $this->aTokens; - } -} diff --git a/lib-php/TokenPartial.php b/lib-php/TokenPartial.php deleted file mode 100644 index 3dc6f308..00000000 --- a/lib-php/TokenPartial.php +++ /dev/null @@ -1,127 +0,0 @@ -iId = $iId; - $this->bNumberToken = (bool) preg_match('#^[0-9 ]+$#', $sToken); - $this->iSearchNameCount = $iSearchNameCount; - } - - public function getId() - { - return $this->iId; - } - - /** - * Check if the token can be added to the given search. - * Derive new searches by adding this token to an existing search. - * - * @param object $oSearch Partial search description derived so far. - * @param object $oPosition Description of the token position within - the query. - * - * @return True if the token is compatible with the search configuration - * given the position. - */ - public function isExtendable($oSearch, $oPosition) - { - return !$oPosition->isPhrase('country'); - } - - /** - * Derive new searches by adding this token to an existing search. - * - * @param object $oSearch Partial search description derived so far. - * @param object $oPosition Description of the token position within - the query. - * - * @return SearchDescription[] List of derived search descriptions. - */ - public function extendSearch($oSearch, $oPosition) - { - $aNewSearches = array(); - - // Partial token in Address. - if (($oPosition->isPhrase('') || !$oPosition->isFirstPhrase()) - && $oSearch->hasName() - ) { - $iSearchCost = $this->bNumberToken ? 2 : 1; - if ($this->iSearchNameCount >= CONST_Max_Word_Frequency) { - $iSearchCost += 1; - } - - $oNewSearch = $oSearch->clone($iSearchCost); - $oNewSearch->addAddressToken( - $this->iId, - $this->iSearchNameCount < CONST_Max_Word_Frequency - ); - - $aNewSearches[] = $oNewSearch; - } - - // Partial token in Name. - if ((!$oSearch->hasPostcode() && !$oSearch->hasAddress()) - && (!$oSearch->hasName(true) - || $oSearch->getNamePhrase() == $oPosition->getPhrase()) - ) { - $iSearchCost = 1; - if (!$oSearch->hasName(true)) { - $iSearchCost += 1; - } - if ($this->bNumberToken) { - $iSearchCost += 1; - } - - $oNewSearch = $oSearch->clone($iSearchCost); - $oNewSearch->addPartialNameToken( - $this->iId, - $this->iSearchNameCount < CONST_Max_Word_Frequency, - $this->iSearchNameCount > CONST_Search_NameOnlySearchFrequencyThreshold, - $oPosition->getPhrase() - ); - - $aNewSearches[] = $oNewSearch; - } - - return $aNewSearches; - } - - - public function debugInfo() - { - return array( - 'ID' => $this->iId, - 'Type' => 'partial', - 'Info' => array( - 'count' => $this->iSearchNameCount - ) - ); - } - - public function debugCode() - { - return 'w'; - } -} diff --git a/lib-php/TokenPostcode.php b/lib-php/TokenPostcode.php deleted file mode 100644 index 0ff92929..00000000 --- a/lib-php/TokenPostcode.php +++ /dev/null @@ -1,111 +0,0 @@ -iId = $iId; - $iSplitPos = strpos($sPostcode, '@'); - if ($iSplitPos === false) { - $this->sPostcode = $sPostcode; - } else { - $this->sPostcode = substr($sPostcode, 0, $iSplitPos); - } - $this->sCountryCode = empty($sCountryCode) ? '' : $sCountryCode; - } - - public function getId() - { - return $this->iId; - } - - /** - * Check if the token can be added to the given search. - * Derive new searches by adding this token to an existing search. - * - * @param object $oSearch Partial search description derived so far. - * @param object $oPosition Description of the token position within - the query. - * - * @return True if the token is compatible with the search configuration - * given the position. - */ - public function isExtendable($oSearch, $oPosition) - { - return !$oSearch->hasPostcode() && $oPosition->maybePhrase('postalcode'); - } - - /** - * Derive new searches by adding this token to an existing search. - * - * @param object $oSearch Partial search description derived so far. - * @param object $oPosition Description of the token position within - the query. - * - * @return SearchDescription[] List of derived search descriptions. - */ - public function extendSearch($oSearch, $oPosition) - { - $aNewSearches = array(); - - // If we have structured search or this is the first term, - // make the postcode the primary search element. - if ($oSearch->hasOperator(\Nominatim\Operator::NONE) && $oPosition->isFirstToken()) { - $oNewSearch = $oSearch->clone(1); - $oNewSearch->setPostcodeAsName($this->iId, $this->sPostcode); - - $aNewSearches[] = $oNewSearch; - } - - // If we have a structured search or this is not the first term, - // add the postcode as an addendum. - if (!$oSearch->hasOperator(\Nominatim\Operator::POSTCODE) - && ($oPosition->isPhrase('postalcode') || $oSearch->hasName()) - ) { - $iPenalty = 1; - if (strlen($this->sPostcode) < 4) { - $iPenalty += 4 - strlen($this->sPostcode); - } - $oNewSearch = $oSearch->clone($iPenalty); - $oNewSearch->setPostcode($this->sPostcode); - - $aNewSearches[] = $oNewSearch; - } - - return $aNewSearches; - } - - public function debugInfo() - { - return array( - 'ID' => $this->iId, - 'Type' => 'postcode', - 'Info' => $this->sPostcode.'('.$this->sCountryCode.')' - ); - } - - public function debugCode() - { - return 'P'; - } -} diff --git a/lib-php/TokenSpecialTerm.php b/lib-php/TokenSpecialTerm.php deleted file mode 100644 index 475ae71b..00000000 --- a/lib-php/TokenSpecialTerm.php +++ /dev/null @@ -1,125 +0,0 @@ -iId = $iID; - $this->sClass = $sClass; - $this->sType = $sType; - $this->iOperator = $iOperator; - } - - public function getId() - { - return $this->iId; - } - - /** - * Check if the token can be added to the given search. - * Derive new searches by adding this token to an existing search. - * - * @param object $oSearch Partial search description derived so far. - * @param object $oPosition Description of the token position within - the query. - * - * @return True if the token is compatible with the search configuration - * given the position. - */ - public function isExtendable($oSearch, $oPosition) - { - return !$oSearch->hasOperator() - && $oPosition->isPhrase('') - && ($this->iOperator != \Nominatim\Operator::NONE - || (!$oSearch->hasAddress() && !$oSearch->hasHousenumber() && !$oSearch->hasCountry())); - } - - /** - * Derive new searches by adding this token to an existing search. - * - * @param object $oSearch Partial search description derived so far. - * @param object $oPosition Description of the token position within - the query. - * - * @return SearchDescription[] List of derived search descriptions. - */ - public function extendSearch($oSearch, $oPosition) - { - $iSearchCost = 0; - - $iOp = $this->iOperator; - if ($iOp == \Nominatim\Operator::NONE) { - if ($oPosition->isFirstToken() - || $oSearch->hasName() - || $oSearch->getContext()->isBoundedSearch() - ) { - $iOp = \Nominatim\Operator::NAME; - $iSearchCost += 3; - } else { - $iOp = \Nominatim\Operator::NEAR; - $iSearchCost += 4; - if (!$oPosition->isFirstToken()) { - $iSearchCost += 3; - } - } - } elseif ($oPosition->isFirstToken()) { - $iSearchCost += 2; - } elseif ($oPosition->isLastToken()) { - $iSearchCost += 4; - } else { - $iSearchCost += 6; - } - - if ($oSearch->hasHousenumber()) { - $iSearchCost ++; - } - - $oNewSearch = $oSearch->clone($iSearchCost); - $oNewSearch->setPoiSearch($iOp, $this->sClass, $this->sType); - - return array($oNewSearch); - } - - - public function debugInfo() - { - return array( - 'ID' => $this->iId, - 'Type' => 'special term', - 'Info' => array( - 'class' => $this->sClass, - 'type' => $this->sType, - 'operator' => \Nominatim\Operator::toString($this->iOperator) - ) - ); - } - - public function debugCode() - { - return 'S'; - } -} diff --git a/lib-php/TokenWord.php b/lib-php/TokenWord.php deleted file mode 100644 index a7557d38..00000000 --- a/lib-php/TokenWord.php +++ /dev/null @@ -1,110 +0,0 @@ -iId = $iId; - $this->iSearchNameCount = $iSearchNameCount; - $this->iTermCount = $iTermCount; - } - - public function getId() - { - return $this->iId; - } - - /** - * Check if the token can be added to the given search. - * Derive new searches by adding this token to an existing search. - * - * @param object $oSearch Partial search description derived so far. - * @param object $oPosition Description of the token position within - the query. - * - * @return True if the token is compatible with the search configuration - * given the position. - */ - public function isExtendable($oSearch, $oPosition) - { - return !$oPosition->isPhrase('country'); - } - - /** - * Derive new searches by adding this token to an existing search. - * - * @param object $oSearch Partial search description derived so far. - * @param object $oPosition Description of the token position within - the query. - * - * @return SearchDescription[] List of derived search descriptions. - */ - public function extendSearch($oSearch, $oPosition) - { - // Full words can only be a name if they appear at the beginning - // of the phrase. In structured search the name must forcibly in - // the first phrase. In unstructured search it may be in a later - // phrase when the first phrase is a house number. - if ($oSearch->hasName() - || !($oPosition->isFirstPhrase() || $oPosition->isPhrase('')) - ) { - if ($this->iTermCount > 1 - && ($oPosition->isPhrase('') || !$oPosition->isFirstPhrase()) - ) { - $oNewSearch = $oSearch->clone(1); - $oNewSearch->addAddressToken($this->iId); - - return array($oNewSearch); - } - } elseif (!$oSearch->hasName(true)) { - $oNewSearch = $oSearch->clone(1); - $oNewSearch->addNameToken( - $this->iId, - CONST_Search_NameOnlySearchFrequencyThreshold - && $this->iSearchNameCount - < CONST_Search_NameOnlySearchFrequencyThreshold - ); - - return array($oNewSearch); - } - - return array(); - } - - public function debugInfo() - { - return array( - 'ID' => $this->iId, - 'Type' => 'word', - 'Info' => array( - 'count' => $this->iSearchNameCount, - 'terms' => $this->iTermCount - ) - ); - } - - public function debugCode() - { - return 'W'; - } -} diff --git a/lib-php/cmd.php b/lib-php/cmd.php deleted file mode 100644 index 6f1299dd..00000000 --- a/lib-php/cmd.php +++ /dev/null @@ -1,199 +0,0 @@ -= $iSize || $aArg[$i][0] == '-') { - showUsage($aSpec, $bExitOnError, 'Parameter of \''.$aLine[0].'\' is missing'); - } - - switch ($aLine[6]) { - case 'realpath': - $xVal[] = realpath($aArg[$i]); - break; - case 'realdir': - $sPath = realpath(dirname($aArg[$i])); - if ($sPath) { - $xVal[] = $sPath . '/' . basename($aArg[$i]); - } else { - $xVal[] = $sPath; - } - break; - case 'bool': - $xVal[] = (bool)$aArg[$i]; - break; - case 'int': - $xVal[] = (int)$aArg[$i]; - break; - case 'float': - $xVal[] = (float)$aArg[$i]; - break; - default: - $xVal[] = $aArg[$i]; - break; - } - } - if ($aLine[4] == 1) { - $xVal = $xVal[0]; - } - } else { - $xVal = true; - } - } else { - fail('Variable numbers of params not yet supported'); - } - - if ($aLine[3] > 1) { - if (!array_key_exists($aLine[0], $aResult)) { - $aResult[$aLine[0]] = array(); - } - $aResult[$aLine[0]][] = $xVal; - } else { - $aResult[$aLine[0]] = $xVal; - } - } else { - $bUnknown = $aArg[$i]; - } - } - - if (array_key_exists('help', $aResult)) { - showUsage($aSpec); - } - if ($bUnknown && $bExitOnUnknown) { - showUsage($aSpec, $bExitOnError, 'Unknown option \''.$bUnknown.'\''); - } - - foreach ($aSpec as $aLine) { - if (is_array($aLine)) { - if ($aCounts[$aLine[0]] < $aLine[2]) { - showUsage($aSpec, $bExitOnError, 'Option \''.$aLine[0].'\' is missing'); - } - if ($aCounts[$aLine[0]] > $aLine[3]) { - showUsage($aSpec, $bExitOnError, 'Option \''.$aLine[0].'\' is present too many times'); - } - if ($aLine[6] == 'bool' && !array_key_exists($aLine[0], $aResult)) { - $aResult[$aLine[0]] = false; - } - } - } - return $bUnknown; -} - -function showUsage($aSpec, $bExit = false, $sError = false) -{ - if ($sError) { - echo basename($_SERVER['argv'][0]).': '.$sError."\n"; - echo 'Try `'.basename($_SERVER['argv'][0]).' --help` for more information.'."\n"; - exit; - } - echo 'Usage: '.basename($_SERVER['argv'][0])."\n"; - $bFirst = true; - foreach ($aSpec as $aLine) { - if (is_array($aLine)) { - if ($bFirst) { - $bFirst = false; - echo "\n"; - } - $aNames = array(); - if ($aLine[1]) { - $aNames[] = '-'.$aLine[1]; - } - if ($aLine[0]) { - $aNames[] = '--'.$aLine[0]; - } - $sName = join(', ', $aNames); - echo ' '.$sName.str_repeat(' ', 30-strlen($sName)).$aLine[7]."\n"; - } else { - echo $aLine."\n"; - } - } - echo "\n"; - exit; -} - -function info($sMsg) -{ - echo date('Y-m-d H:i:s == ').$sMsg."\n"; -} - -$aWarnings = array(); - - -function warn($sMsg) -{ - $GLOBALS['aWarnings'][] = $sMsg; - echo date('Y-m-d H:i:s == ').'WARNING: '.$sMsg."\n"; -} - - -function repeatWarnings() -{ - foreach ($GLOBALS['aWarnings'] as $sMsg) { - echo ' * ',$sMsg."\n"; - } -} - - -function setupHTTPProxy() -{ - if (!getSettingBool('HTTP_PROXY')) { - return; - } - - $sProxy = 'tcp://'.getSetting('HTTP_PROXY_HOST').':'.getSetting('HTTP_PROXY_PROT'); - $aHeaders = array(); - - $sLogin = getSetting('HTTP_PROXY_LOGIN'); - $sPassword = getSetting('HTTP_PROXY_PASSWORD'); - - if ($sLogin && $sPassword) { - $sAuth = base64_encode($sLogin.':'.$sPassword); - $aHeaders = array('Proxy-Authorization: Basic '.$sAuth); - } - - $aProxyHeader = array( - 'proxy' => $sProxy, - 'request_fulluri' => true, - 'header' => $aHeaders - ); - - $aContext = array('http' => $aProxyHeader, 'https' => $aProxyHeader); - stream_context_set_default($aContext); -} diff --git a/lib-php/dotenv_loader.php b/lib-php/dotenv_loader.php deleted file mode 100644 index bcddf008..00000000 --- a/lib-php/dotenv_loader.php +++ /dev/null @@ -1,21 +0,0 @@ -load(CONST_ConfigDir.'/env.defaults'); - - if (file_exists('.env')) { - $dotenv->load('.env'); - } -} diff --git a/lib-php/init-cmd.php b/lib-php/init-cmd.php deleted file mode 100644 index 44e7adb2..00000000 --- a/lib-php/init-cmd.php +++ /dev/null @@ -1,13 +0,0 @@ -getCode() == 0 ? 500 : $exception->getCode()); - header('Content-type: application/json; charset=utf-8'); - include(CONST_LibDir.'/template/error-json.php'); - exit(); -} - -function exception_handler_xml($exception) -{ - http_response_code($exception->getCode() == 0 ? 500 : $exception->getCode()); - header('Content-type: text/xml; charset=utf-8'); - echo ''."\n"; - include(CONST_LibDir.'/template/error-xml.php'); - exit(); -} - -function shutdown_exception_handler_xml() -{ - $error = error_get_last(); - if ($error !== null && $error['type'] === E_ERROR) { - exception_handler_xml(new \Exception($error['message'], 500)); - } -} - -function shutdown_exception_handler_json() -{ - $error = error_get_last(); - if ($error !== null && $error['type'] === E_ERROR) { - exception_handler_json(new \Exception($error['message'], 500)); - } -} - - -function set_exception_handler_by_format($sFormat = null) -{ - // Multiple calls to register_shutdown_function will cause multiple callbacks - // to be executed, we only want the last executed. Thus we don't want to register - // one by default without an explicit $sFormat set. - - if (!isset($sFormat)) { - set_exception_handler('exception_handler_json'); - } elseif ($sFormat == 'xml') { - set_exception_handler('exception_handler_xml'); - register_shutdown_function('shutdown_exception_handler_xml'); - } else { - set_exception_handler('exception_handler_json'); - register_shutdown_function('shutdown_exception_handler_json'); - } -} -// set a default -set_exception_handler_by_format(); - - -/*************************************************************************** - * HTTP Reply header setup - */ - -if (CONST_NoAccessControl) { - header('Access-Control-Allow-Origin: *'); - header('Access-Control-Allow-Methods: OPTIONS,GET'); - if (!empty($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) { - header('Access-Control-Allow-Headers: '.$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']); - } -} -if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] == 'OPTIONS') { - exit; -} - -if (CONST_Debug) { - header('Content-type: text/html; charset=utf-8'); -} diff --git a/lib-php/init.php b/lib-php/init.php deleted file mode 100644 index 9e71a761..00000000 --- a/lib-php/init.php +++ /dev/null @@ -1,12 +0,0 @@ -getOne("select max(osm_id) from place where osm_type = 'N'"); - // Lookup the timestamp that node was created - $sLastNodeURL = 'https://www.openstreetmap.org/api/0.6/node/'.$iLastOSMID.'/1'; - $sLastNodeXML = file_get_contents($sLastNodeURL); - - if ($sLastNodeXML === false) { - return false; - } - - preg_match('#timestamp="(([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})Z)"#', $sLastNodeXML, $aLastNodeDate); - - return $aLastNodeDate[1]; -} - - -function byImportance($a, $b) -{ - if ($a['importance'] != $b['importance']) { - return ($a['importance'] > $b['importance']?-1:1); - } - - return $a['foundorder'] <=> $b['foundorder']; -} - - -function javascript_renderData($xVal, $iOptions = 0) -{ - $sCallback = isset($_GET['json_callback']) ? $_GET['json_callback'] : ''; - if ($sCallback && !preg_match('/^[$_\p{L}][$_\p{L}\p{Nd}.[\]]*$/u', $sCallback)) { - // Unset, we call javascript_renderData again during exception handling - unset($_GET['json_callback']); - throw new Exception('Invalid json_callback value', 400); - } - - $iOptions |= JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; - if (isset($_GET['pretty']) && in_array(strtolower($_GET['pretty']), array('1', 'true'))) { - $iOptions |= JSON_PRETTY_PRINT; - } - - $jsonout = json_encode($xVal, $iOptions); - - if ($sCallback) { - header('Content-Type: application/javascript; charset=UTF-8'); - echo $_GET['json_callback'].'('.$jsonout.')'; - } else { - header('Content-Type: application/json; charset=UTF-8'); - echo $jsonout; - } -} - -function addQuotes($s) -{ - return "'".$s."'"; -} - -function parseLatLon($sQuery) -{ - $sFound = null; - $fQueryLat = null; - $fQueryLon = null; - - if (preg_match('/\\s*([NS])[\s]+([0-9]+[0-9.]*)[°\s]+([0-9.]+)?[′\']*[,\s]+([EW])[\s]+([0-9]+)[°\s]+([0-9]+[0-9.]*)[′\']*\\s*/', $sQuery, $aData)) { - /* 1 2 3 4 5 6 - * degrees decimal minutes - * N 40 26.767, W 79 58.933 - * N 40°26.767′, W 79°58.933′ - */ - $sFound = $aData[0]; - $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60); - $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60); - } elseif (preg_match('/\\s*([0-9]+)[°\s]+([0-9]+[0-9.]*)?[′\']*[\s]+([NS])[,\s]+([0-9]+)[°\s]+([0-9]+[0-9.]*)?[′\'\s]+([EW])\\s*/', $sQuery, $aData)) { - /* 1 2 3 4 5 6 - * degrees decimal minutes - * 40 26.767 N, 79 58.933 W - * 40° 26.767′ N 79° 58.933′ W - */ - $sFound = $aData[0]; - $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60); - $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60); - } elseif (preg_match('/\\s*([NS])[\s]+([0-9]+)[°\s]+([0-9]+)[′\'\s]+([0-9]+)[″"]*[,\s]+([EW])[\s]+([0-9]+)[°\s]+([0-9]+)[′\'\s]+([0-9]+)[″"]*\\s*/', $sQuery, $aData)) { - /* 1 2 3 4 5 6 7 8 - * degrees decimal seconds - * N 40 26 46 W 79 58 56 - * N 40° 26′ 46″, W 79° 58′ 56″ - */ - $sFound = $aData[0]; - $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60 + $aData[4]/3600); - $fQueryLon = ($aData[5]=='E'?1:-1) * ($aData[6] + $aData[7]/60 + $aData[8]/3600); - } elseif (preg_match('/\\s*([0-9]+)[°\s]+([0-9]+)[′\'\s]+([0-9]+[0-9.]*)[″"\s]+([NS])[,\s]+([0-9]+)[°\s]+([0-9]+)[′\'\s]+([0-9]+[0-9.]*)[″"\s]+([EW])\\s*/', $sQuery, $aData)) { - /* 1 2 3 4 5 6 7 8 - * degrees decimal seconds - * 40 26 46 N 79 58 56 W - * 40° 26′ 46″ N, 79° 58′ 56″ W - * 40° 26′ 46.78″ N, 79° 58′ 56.89″ W - */ - $sFound = $aData[0]; - $fQueryLat = ($aData[4]=='N'?1:-1) * ($aData[1] + $aData[2]/60 + $aData[3]/3600); - $fQueryLon = ($aData[8]=='E'?1:-1) * ($aData[5] + $aData[6]/60 + $aData[7]/3600); - } elseif (preg_match('/\\s*([NS])[\s]+([0-9]+[0-9]*\\.[0-9]+)[°]*[,\s]+([EW])[\s]+([0-9]+[0-9]*\\.[0-9]+)[°]*\\s*/', $sQuery, $aData)) { - /* 1 2 3 4 - * degrees decimal - * N 40.446° W 79.982° - */ - $sFound = $aData[0]; - $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2]); - $fQueryLon = ($aData[3]=='E'?1:-1) * ($aData[4]); - } elseif (preg_match('/\\s*([0-9]+[0-9]*\\.[0-9]+)[°\s]+([NS])[,\s]+([0-9]+[0-9]*\\.[0-9]+)[°\s]+([EW])\\s*/', $sQuery, $aData)) { - /* 1 2 3 4 - * degrees decimal - * 40.446° N 79.982° W - */ - $sFound = $aData[0]; - $fQueryLat = ($aData[2]=='N'?1:-1) * ($aData[1]); - $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[3]); - } elseif (preg_match('/(\\s*\\[|^\\s*|\\s*)(-?[0-9]+[0-9]*\\.[0-9]+)[,\s]+(-?[0-9]+[0-9]*\\.[0-9]+)(\\]\\s*|\\s*$|\\s*)/', $sQuery, $aData)) { - /* 1 2 3 4 - * degrees decimal - * 12.34, 56.78 - * 12.34 56.78 - * [12.456,-78.90] - */ - $sFound = $aData[0]; - $fQueryLat = $aData[2]; - $fQueryLon = $aData[3]; - } else { - return false; - } - - return array($sFound, $fQueryLat, $fQueryLon); -} - -function addressRankToGeocodeJsonType($iAddressRank) -{ - if ($iAddressRank >= 29 && $iAddressRank <= 30) { - return 'house'; - } - if ($iAddressRank >= 26 && $iAddressRank < 28) { - return 'street'; - } - if ($iAddressRank >= 22 && $iAddressRank < 26) { - return 'locality'; - } - if ($iAddressRank >= 17 && $iAddressRank < 22) { - return 'district'; - } - if ($iAddressRank >= 13 && $iAddressRank < 17) { - return 'city'; - } - if ($iAddressRank >= 10 && $iAddressRank < 13) { - return 'county'; - } - if ($iAddressRank >= 5 && $iAddressRank < 10) { - return 'state'; - } - if ($iAddressRank >= 4 && $iAddressRank < 5) { - return 'country'; - } - - return 'locality'; -} - -if (!function_exists('array_key_last')) { - function array_key_last(array $array) - { - if (!empty($array)) { - return key(array_slice($array, -1, 1, true)); - } - } -} diff --git a/lib-php/log.php b/lib-php/log.php deleted file mode 100644 index 1d567733..00000000 --- a/lib-php/log.php +++ /dev/null @@ -1,104 +0,0 @@ -getDBQuotedList(array( - $sType, - $hLog[0], - $hLog[2], - $hLog[1], - $sUserAgent, - join(',', $aLanguageList), - $sOutputFormat, - $hLog[3] - ))); - $sSQL .= ')'; - $oDB->exec($sSQL); - } - - return $hLog; -} - -function logEnd(&$oDB, $hLog, $iNumResults) -{ - $fEndTime = microtime(true); - - if (CONST_Log_DB) { - $aEndTime = explode('.', $fEndTime); - if (!isset($aEndTime[1])) { - $aEndTime[1] = '0'; - } - $sEndTime = date('Y-m-d H:i:s', $aEndTime[0]).'.'.$aEndTime[1]; - - $sSQL = 'update new_query_log set endtime = '.$oDB->getDBQuoted($sEndTime).', results = '.$iNumResults; - $sSQL .= ' where starttime = '.$oDB->getDBQuoted($hLog[0]); - $sSQL .= ' and ipaddress = '.$oDB->getDBQuoted($hLog[1]); - $sSQL .= ' and query = '.$oDB->getDBQuoted($hLog[2]); - $oDB->exec($sSQL); - } - - if (CONST_Log_File) { - $aOutdata = sprintf( - "[%s] %.4f %d %s \"%s\"\n", - $hLog[0], - $fEndTime-$hLog[5], - $iNumResults, - $hLog[4], - $hLog[2] - ); - file_put_contents(CONST_Log_File, $aOutdata, FILE_APPEND | LOCK_EX); - } -} diff --git a/lib-php/output.php b/lib-php/output.php deleted file mode 100644 index 44c4dde8..00000000 --- a/lib-php/output.php +++ /dev/null @@ -1,38 +0,0 @@ - 'Feature', - 'properties' => array( - 'geocoding' => array() - ) - ); - - if (isset($aPlace['place_id'])) { - $aFilteredPlaces['properties']['geocoding']['place_id'] = $aPlace['place_id']; - } - $sOSMType = formatOSMType($aPlace['osm_type']); - if ($sOSMType) { - $aFilteredPlaces['properties']['geocoding']['osm_type'] = $sOSMType; - $aFilteredPlaces['properties']['geocoding']['osm_id'] = $aPlace['osm_id']; - } - - $aFilteredPlaces['properties']['geocoding']['osm_key'] = $aPlace['class']; - $aFilteredPlaces['properties']['geocoding']['osm_value'] = $aPlace['type']; - - $aFilteredPlaces['properties']['geocoding']['type'] = addressRankToGeocodeJsonType($aPlace['rank_address']); - - $aFilteredPlaces['properties']['geocoding']['accuracy'] = (int) $fDistance; - - $aFilteredPlaces['properties']['geocoding']['label'] = $aPlace['langaddress']; - - if ($aPlace['placename'] !== null) { - $aFilteredPlaces['properties']['geocoding']['name'] = $aPlace['placename']; - } - - if (isset($aPlace['address'])) { - $aPlace['address']->addGeocodeJsonAddressParts( - $aFilteredPlaces['properties']['geocoding'] - ); - - $aFilteredPlaces['properties']['geocoding']['admin'] - = $aPlace['address']->getAdminLevels(); - } - - if (isset($aPlace['asgeojson'])) { - $aFilteredPlaces['geometry'] = json_decode($aPlace['asgeojson'], true); - } else { - $aFilteredPlaces['geometry'] = array( - 'type' => 'Point', - 'coordinates' => array( - (float) $aPlace['lon'], - (float) $aPlace['lat'] - ) - ); - } - - javascript_renderData(array( - 'type' => 'FeatureCollection', - 'geocoding' => array( - 'version' => '0.1.0', - 'attribution' => 'Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright', - 'licence' => 'ODbL', - 'query' => $sQuery - ), - 'features' => array($aFilteredPlaces) - )); -} diff --git a/lib-php/template/address-geojson.php b/lib-php/template/address-geojson.php deleted file mode 100644 index dc3c3832..00000000 --- a/lib-php/template/address-geojson.php +++ /dev/null @@ -1,85 +0,0 @@ - 'Feature', - 'properties' => array() - ); - - if (isset($aPlace['place_id'])) { - $aFilteredPlaces['properties']['place_id'] = $aPlace['place_id']; - } - $sOSMType = formatOSMType($aPlace['osm_type']); - if ($sOSMType) { - $aFilteredPlaces['properties']['osm_type'] = $sOSMType; - $aFilteredPlaces['properties']['osm_id'] = $aPlace['osm_id']; - } - - $aFilteredPlaces['properties']['place_rank'] = $aPlace['rank_search']; - - $aFilteredPlaces['properties']['category'] = $aPlace['class']; - $aFilteredPlaces['properties']['type'] = $aPlace['type']; - - $aFilteredPlaces['properties']['importance'] = $aPlace['importance']; - - $aFilteredPlaces['properties']['addresstype'] = strtolower($aPlace['addresstype']); - - $aFilteredPlaces['properties']['name'] = $aPlace['placename']; - - $aFilteredPlaces['properties']['display_name'] = $aPlace['langaddress']; - - if (isset($aPlace['address'])) { - $aFilteredPlaces['properties']['address'] = $aPlace['address']->getAddressNames(); - } - if (isset($aPlace['sExtraTags'])) { - $aFilteredPlaces['properties']['extratags'] = $aPlace['sExtraTags']; - } - if (isset($aPlace['sNameDetails'])) { - $aFilteredPlaces['properties']['namedetails'] = $aPlace['sNameDetails']; - } - - if (isset($aPlace['aBoundingBox'])) { - $aFilteredPlaces['bbox'] = array( - (float) $aPlace['aBoundingBox'][2], // minlon - (float) $aPlace['aBoundingBox'][0], // minlat - (float) $aPlace['aBoundingBox'][3], // maxlon - (float) $aPlace['aBoundingBox'][1] // maxlat - ); - } - - if (isset($aPlace['asgeojson'])) { - $aFilteredPlaces['geometry'] = json_decode($aPlace['asgeojson'], true); - } else { - $aFilteredPlaces['geometry'] = array( - 'type' => 'Point', - 'coordinates' => array( - (float) $aPlace['lon'], - (float) $aPlace['lat'] - ) - ); - } - - - javascript_renderData(array( - 'type' => 'FeatureCollection', - 'licence' => 'Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright', - 'features' => array($aFilteredPlaces) - )); -} diff --git a/lib-php/template/address-json.php b/lib-php/template/address-json.php deleted file mode 100644 index 0766eaf4..00000000 --- a/lib-php/template/address-json.php +++ /dev/null @@ -1,82 +0,0 @@ -getAddressNames(); - } - if (isset($aPlace['sExtraTags'])) { - $aFilteredPlaces['extratags'] = $aPlace['sExtraTags']; - } - if (isset($aPlace['sNameDetails'])) { - $aFilteredPlaces['namedetails'] = $aPlace['sNameDetails']; - } - - if (isset($aPlace['aBoundingBox'])) { - $aFilteredPlaces['boundingbox'] = $aPlace['aBoundingBox']; - } - - if (isset($aPlace['asgeojson'])) { - $aFilteredPlaces['geojson'] = json_decode($aPlace['asgeojson'], true); - } - - if (isset($aPlace['assvg'])) { - $aFilteredPlaces['svg'] = $aPlace['assvg']; - } - - if (isset($aPlace['astext'])) { - $aFilteredPlaces['geotext'] = $aPlace['astext']; - } - - if (isset($aPlace['askml'])) { - $aFilteredPlaces['geokml'] = $aPlace['askml']; - } -} - -javascript_renderData($aFilteredPlaces); diff --git a/lib-php/template/address-xml.php b/lib-php/template/address-xml.php deleted file mode 100644 index c418a4c4..00000000 --- a/lib-php/template/address-xml.php +++ /dev/null @@ -1,110 +0,0 @@ -\n"; - -echo '\n"; - -if (empty($aPlace)) { - if (isset($sError)) { - echo "$sError"; - } else { - echo 'Unable to geocode'; - } -} else { - echo ''.htmlspecialchars($aPlace['langaddress']).''; - - if (isset($aPlace['address'])) { - echo ''; - foreach ($aPlace['address']->getAddressNames() as $sKey => $sValue) { - $sKey = str_replace(' ', '_', $sKey); - echo "<$sKey>"; - echo htmlspecialchars($sValue); - echo ""; - } - echo ''; - } - - if (isset($aPlace['sExtraTags'])) { - echo ''; - foreach ($aPlace['sExtraTags'] as $sKey => $sValue) { - echo ''; - } - echo ''; - } - - if (isset($aPlace['sNameDetails'])) { - echo ''; - foreach ($aPlace['sNameDetails'] as $sKey => $sValue) { - echo ''; - echo htmlspecialchars($sValue); - echo ''; - } - echo ''; - } - - if (isset($aPlace['askml'])) { - echo "\n"; - echo $aPlace['askml']; - echo ''; - } -} - -echo ''; diff --git a/lib-php/template/details-json.php b/lib-php/template/details-json.php deleted file mode 100644 index ae80a85b..00000000 --- a/lib-php/template/details-json.php +++ /dev/null @@ -1,120 +0,0 @@ -format(DateTime::RFC3339); -$aPlaceDetails['importance'] = (float) $aPointDetails['importance']; -$aPlaceDetails['calculated_importance'] = (float) $aPointDetails['calculated_importance']; - -$aPlaceDetails['extratags'] = $aPointDetails['aExtraTags']; -$aPlaceDetails['calculated_wikipedia'] = $aPointDetails['wikipedia']; -$sIcon = Nominatim\ClassTypes\getIconFile($aPointDetails); -if (isset($sIcon)) { - $aPlaceDetails['icon'] = $sIcon; -} - -$aPlaceDetails['rank_address'] = (int) $aPointDetails['rank_address']; -$aPlaceDetails['rank_search'] = (int) $aPointDetails['rank_search']; - -$aPlaceDetails['isarea'] = $aPointDetails['isarea']; -$aPlaceDetails['centroid'] = array( - 'type' => 'Point', - 'coordinates' => array( (float) $aPointDetails['lon'], (float) $aPointDetails['lat'] ) - ); - -$aPlaceDetails['geometry'] = json_decode($aPointDetails['asgeojson'], true); - -$funcMapAddressLine = function ($aFull) { - return array( - 'localname' => $aFull['localname'], - 'place_id' => isset($aFull['place_id']) ? (int) $aFull['place_id'] : null, - 'osm_id' => isset($aFull['osm_id']) ? (int) $aFull['osm_id'] : null, - 'osm_type' => isset($aFull['osm_type']) ? $aFull['osm_type'] : null, - 'place_type' => isset($aFull['place_type']) ? $aFull['place_type'] : null, - 'class' => $aFull['class'], - 'type' => $aFull['type'], - 'admin_level' => isset($aFull['admin_level']) ? (int) $aFull['admin_level'] : null, - 'rank_address' => $aFull['rank_address'] ? (int) $aFull['rank_address'] : null, - 'distance' => (float) $aFull['distance'], - 'isaddress' => isset($aFull['isaddress']) ? (bool) $aFull['isaddress'] : null - ); -}; - -$funcMapKeyword = function ($aFull) { - return array( - 'id' => (int) $aFull['word_id'], - 'token' => $aFull['word_token'] - ); -}; - -if ($aAddressLines) { - $aPlaceDetails['address'] = array_map($funcMapAddressLine, $aAddressLines); -} - -if ($aLinkedLines) { - $aPlaceDetails['linked_places'] = array_map($funcMapAddressLine, $aLinkedLines); -} - -if ($bIncludeKeywords) { - $aPlaceDetails['keywords'] = array(); - - if ($aPlaceSearchNameKeywords) { - $aPlaceDetails['keywords']['name'] = array_map($funcMapKeyword, $aPlaceSearchNameKeywords); - } else { - $aPlaceDetails['keywords']['name'] = array(); - } - - if ($aPlaceSearchAddressKeywords) { - $aPlaceDetails['keywords']['address'] = array_map($funcMapKeyword, $aPlaceSearchAddressKeywords); - } else { - $aPlaceDetails['keywords']['address'] = array(); - } -} - -if ($bIncludeHierarchy) { - if ($bGroupHierarchy) { - $aPlaceDetails['hierarchy'] = array(); - foreach ($aHierarchyLines as $aAddressLine) { - if ($aAddressLine['type'] == 'yes') { - $sType = $aAddressLine['class']; - } else { - $sType = $aAddressLine['type']; - } - - if (!isset($aPlaceDetails['hierarchy'][$sType])) { - $aPlaceDetails['hierarchy'][$sType] = array(); - } - $aPlaceDetails['hierarchy'][$sType][] = $funcMapAddressLine($aAddressLine); - } - } else { - $aPlaceDetails['hierarchy'] = array_map($funcMapAddressLine, $aHierarchyLines); - } -} - -javascript_renderData($aPlaceDetails); diff --git a/lib-php/template/error-json.php b/lib-php/template/error-json.php deleted file mode 100644 index fea7d5c5..00000000 --- a/lib-php/template/error-json.php +++ /dev/null @@ -1,19 +0,0 @@ - $exception->getCode(), - 'message' => $exception->getMessage() - ); - - if (CONST_Debug) { - $error['details'] = $exception->getFile() . '('. $exception->getLine() . ')'; - } - - javascript_renderData(array('error' => $error)); diff --git a/lib-php/template/error-xml.php b/lib-php/template/error-xml.php deleted file mode 100644 index a21ac198..00000000 --- a/lib-php/template/error-xml.php +++ /dev/null @@ -1,7 +0,0 @@ - - getCode() ?> - getMessage() ?> - -
getFile() . '('. $exception->getLine() . ')' ?>
- -
\ No newline at end of file diff --git a/lib-php/template/search-batch-json.php b/lib-php/template/search-batch-json.php deleted file mode 100644 index 430237a2..00000000 --- a/lib-php/template/search-batch-json.php +++ /dev/null @@ -1,83 +0,0 @@ - $aPointDetails) { - $aPlace = array( - 'place_id'=>$aPointDetails['place_id'], - ); - - $sOSMType = formatOSMType($aPointDetails['osm_type']); - if ($sOSMType) { - $aPlace['osm_type'] = $sOSMType; - $aPlace['osm_id'] = $aPointDetails['osm_id']; - } - - if (isset($aPointDetails['aBoundingBox'])) { - $aPlace['boundingbox'] = array( - $aPointDetails['aBoundingBox'][0], - $aPointDetails['aBoundingBox'][1], - $aPointDetails['aBoundingBox'][2], - $aPointDetails['aBoundingBox'][3] - ); - } - - if (isset($aPointDetails['zoom'])) { - $aPlace['zoom'] = $aPointDetails['zoom']; - } - - $aPlace['lat'] = $aPointDetails['lat']; - $aPlace['lon'] = $aPointDetails['lon']; - $aPlace['display_name'] = $aPointDetails['name']; - $aPlace['place_rank'] = $aPointDetails['rank_search']; - - $aPlace['category'] = $aPointDetails['class']; - $aPlace['type'] = $aPointDetails['type']; - - $aPlace['importance'] = $aPointDetails['importance']; - - if (isset($aPointDetails['icon'])) { - $aPlace['icon'] = $aPointDetails['icon']; - } - - if (isset($aPointDetails['address'])) { - $aPlace['address'] = $aPointDetails['address']->getAddressNames(); - } - - if (isset($aPointDetails['asgeojson'])) { - $aPlace['geojson'] = json_decode($aPointDetails['asgeojson'], true); - } - - if (isset($aPointDetails['assvg'])) { - $aPlace['svg'] = $aPointDetails['assvg']; - } - - if (isset($aPointDetails['astext'])) { - $aPlace['geotext'] = $aPointDetails['astext']; - } - - if (isset($aPointDetails['askml'])) { - $aPlace['geokml'] = $aPointDetails['askml']; - } - - $aFilteredPlaces[] = $aPlace; - } - $aOutput['batch'][] = $aFilteredPlaces; -} - -javascript_renderData($aOutput, array('geojson')); diff --git a/lib-php/template/search-geocodejson.php b/lib-php/template/search-geocodejson.php deleted file mode 100644 index bba41a0d..00000000 --- a/lib-php/template/search-geocodejson.php +++ /dev/null @@ -1,72 +0,0 @@ - $aPointDetails) { - $aPlace = array( - 'type' => 'Feature', - 'properties' => array( - 'geocoding' => array() - ) - ); - - if (isset($aPointDetails['place_id'])) { - $aPlace['properties']['geocoding']['place_id'] = $aPointDetails['place_id']; - } - $sOSMType = formatOSMType($aPointDetails['osm_type']); - if ($sOSMType) { - $aPlace['properties']['geocoding']['osm_type'] = $sOSMType; - $aPlace['properties']['geocoding']['osm_id'] = $aPointDetails['osm_id']; - } - $aPlace['properties']['geocoding']['osm_key'] = $aPointDetails['class']; - $aPlace['properties']['geocoding']['osm_value'] = $aPointDetails['type']; - - $aPlace['properties']['geocoding']['type'] = addressRankToGeocodeJsonType($aPointDetails['rank_address']); - - $aPlace['properties']['geocoding']['label'] = $aPointDetails['langaddress']; - - if ($aPointDetails['placename'] !== null) { - $aPlace['properties']['geocoding']['name'] = $aPointDetails['placename']; - } - - if (isset($aPointDetails['address'])) { - $aPointDetails['address']->addGeocodeJsonAddressParts( - $aPlace['properties']['geocoding'] - ); - - $aPlace['properties']['geocoding']['admin'] - = $aPointDetails['address']->getAdminLevels(); - } - - if (isset($aPointDetails['asgeojson'])) { - $aPlace['geometry'] = json_decode($aPointDetails['asgeojson'], true); - } else { - $aPlace['geometry'] = array( - 'type' => 'Point', - 'coordinates' => array( - (float) $aPointDetails['lon'], - (float) $aPointDetails['lat'] - ) - ); - } - $aFilteredPlaces[] = $aPlace; -} - - -javascript_renderData(array( - 'type' => 'FeatureCollection', - 'geocoding' => array( - 'version' => '0.1.0', - 'attribution' => 'Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright', - 'licence' => 'ODbL', - 'query' => $sQuery - ), - 'features' => $aFilteredPlaces - )); diff --git a/lib-php/template/search-geojson.php b/lib-php/template/search-geojson.php deleted file mode 100644 index 7665700d..00000000 --- a/lib-php/template/search-geojson.php +++ /dev/null @@ -1,83 +0,0 @@ - $aPointDetails) { - $aPlace = array( - 'type' => 'Feature', - 'properties' => array( - 'place_id'=>$aPointDetails['place_id'], - ) - ); - - $sOSMType = formatOSMType($aPointDetails['osm_type']); - if ($sOSMType) { - $aPlace['properties']['osm_type'] = $sOSMType; - $aPlace['properties']['osm_id'] = $aPointDetails['osm_id']; - } - - if (isset($aPointDetails['aBoundingBox'])) { - $aPlace['bbox'] = array( - (float) $aPointDetails['aBoundingBox'][2], // minlon - (float) $aPointDetails['aBoundingBox'][0], // minlat - (float) $aPointDetails['aBoundingBox'][3], // maxlon - (float) $aPointDetails['aBoundingBox'][1] // maxlat - ); - } - - if (isset($aPointDetails['zoom'])) { - $aPlace['properties']['zoom'] = $aPointDetails['zoom']; - } - - $aPlace['properties']['display_name'] = $aPointDetails['name']; - - $aPlace['properties']['place_rank'] = $aPointDetails['rank_search']; - $aPlace['properties']['category'] = $aPointDetails['class']; - - $aPlace['properties']['type'] = $aPointDetails['type']; - - $aPlace['properties']['importance'] = $aPointDetails['importance']; - - if (isset($aPointDetails['icon']) && $aPointDetails['icon']) { - $aPlace['properties']['icon'] = $aPointDetails['icon']; - } - - if (isset($aPointDetails['address'])) { - $aPlace['properties']['address'] = $aPointDetails['address']->getAddressNames(); - } - - if (isset($aPointDetails['asgeojson'])) { - $aPlace['geometry'] = json_decode($aPointDetails['asgeojson'], true); - } else { - $aPlace['geometry'] = array( - 'type' => 'Point', - 'coordinates' => array( - (float) $aPointDetails['lon'], - (float) $aPointDetails['lat'] - ) - ); - } - - - if (isset($aPointDetails['sExtraTags'])) { - $aPlace['properties']['extratags'] = $aPointDetails['sExtraTags']; - } - if (isset($aPointDetails['sNameDetails'])) { - $aPlace['properties']['namedetails'] = $aPointDetails['sNameDetails']; - } - - $aFilteredPlaces[] = $aPlace; -} - -javascript_renderData(array( - 'type' => 'FeatureCollection', - 'licence' => 'Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright', - 'features' => $aFilteredPlaces - )); diff --git a/lib-php/template/search-json.php b/lib-php/template/search-json.php deleted file mode 100644 index 5fb13020..00000000 --- a/lib-php/template/search-json.php +++ /dev/null @@ -1,81 +0,0 @@ - $aPointDetails) { - $aPlace = array( - 'place_id'=>$aPointDetails['place_id'], - 'licence'=>'Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright', - ); - - $sOSMType = formatOSMType($aPointDetails['osm_type']); - if ($sOSMType) { - $aPlace['osm_type'] = $sOSMType; - $aPlace['osm_id'] = $aPointDetails['osm_id']; - } - - if (isset($aPointDetails['aBoundingBox'])) { - $aPlace['boundingbox'] = $aPointDetails['aBoundingBox']; - } - - if (isset($aPointDetails['zoom'])) { - $aPlace['zoom'] = $aPointDetails['zoom']; - } - - $aPlace['lat'] = $aPointDetails['lat']; - $aPlace['lon'] = $aPointDetails['lon']; - - $aPlace['display_name'] = $aPointDetails['name']; - - if ($sOutputFormat == 'jsonv2' || $sOutputFormat == 'geojson') { - $aPlace['place_rank'] = $aPointDetails['rank_search']; - $aPlace['category'] = $aPointDetails['class']; - } else { - $aPlace['class'] = $aPointDetails['class']; - } - $aPlace['type'] = $aPointDetails['type']; - - $aPlace['importance'] = $aPointDetails['importance']; - - if (isset($aPointDetails['icon']) && $aPointDetails['icon']) { - $aPlace['icon'] = $aPointDetails['icon']; - } - - if (isset($aPointDetails['address'])) { - $aPlace['address'] = $aPointDetails['address']->getAddressNames(); - } - - if (isset($aPointDetails['asgeojson'])) { - $aPlace['geojson'] = json_decode($aPointDetails['asgeojson'], true); - } - - if (isset($aPointDetails['assvg'])) { - $aPlace['svg'] = $aPointDetails['assvg']; - } - - if (isset($aPointDetails['astext'])) { - $aPlace['geotext'] = $aPointDetails['astext']; - } - - if (isset($aPointDetails['askml'])) { - $aPlace['geokml'] = $aPointDetails['askml']; - } - - if (isset($aPointDetails['sExtraTags'])) { - $aPlace['extratags'] = $aPointDetails['sExtraTags']; - } - if (isset($aPointDetails['sNameDetails'])) { - $aPlace['namedetails'] = $aPointDetails['sNameDetails']; - } - - $aFilteredPlaces[] = $aPlace; -} - -javascript_renderData($aFilteredPlaces); diff --git a/lib-php/template/search-xml.php b/lib-php/template/search-xml.php deleted file mode 100644 index 8dda65e2..00000000 --- a/lib-php/template/search-xml.php +++ /dev/null @@ -1,138 +0,0 @@ -\n"; - -echo '<'; -echo (isset($sXmlRootTag)?$sXmlRootTag:'searchresults'); -echo " timestamp='".date(DATE_RFC822)."'"; -echo " attribution='Data © OpenStreetMap contributors, ODbL 1.0. http://www.openstreetmap.org/copyright'"; -echo " querystring='".htmlspecialchars($sQuery, ENT_QUOTES)."'"; -if (isset($aMoreParams['viewbox'])) { - echo " viewbox='".htmlspecialchars($aMoreParams['viewbox'], ENT_QUOTES)."'"; -} -if (isset($aMoreParams['exclude_place_ids'])) { - echo " exclude_place_ids='".htmlspecialchars($aMoreParams['exclude_place_ids'])."'"; -} -echo " more_url='".htmlspecialchars($sMoreURL)."'"; -echo ">\n"; - -foreach ($aSearchResults as $iResNum => $aResult) { - echo "'; - } - echo "\n"; - echo $aResult['askml']; - echo ''; - } - - if (isset($aResult['sExtraTags'])) { - if (!$bHasDelim) { - $bHasDelim = true; - echo '>'; - } - echo "\n"; - foreach ($aResult['sExtraTags'] as $sKey => $sValue) { - echo ''; - } - echo ''; - } - - if (isset($aResult['sNameDetails'])) { - if (!$bHasDelim) { - $bHasDelim = true; - echo '>'; - } - echo "\n"; - foreach ($aResult['sNameDetails'] as $sKey => $sValue) { - echo ''; - echo htmlspecialchars($sValue); - echo ''; - } - echo ''; - } - - if (isset($aResult['address'])) { - if (!$bHasDelim) { - $bHasDelim = true; - echo '>'; - } - echo "\n"; - foreach ($aResult['address']->getAddressNames() as $sKey => $sValue) { - $sKey = str_replace(' ', '_', $sKey); - echo "<$sKey>"; - echo htmlspecialchars($sValue); - echo ""; - } - } - - if ($bHasDelim) { - echo ''; - } else { - echo '/>'; - } -} - -echo ''; diff --git a/lib-php/tokenizer/icu_tokenizer.php b/lib-php/tokenizer/icu_tokenizer.php deleted file mode 100644 index e45d0765..00000000 --- a/lib-php/tokenizer/icu_tokenizer.php +++ /dev/null @@ -1,235 +0,0 @@ -oDB =& $oDB; - $this->oNormalizer = \Transliterator::createFromRules(CONST_Term_Normalization_Rules); - $this->oTransliterator = \Transliterator::createFromRules(CONST_Transliteration); - } - - public function checkStatus() - { - $sSQL = 'SELECT word_id FROM word WHERE word_id is not null limit 1'; - $iWordID = $this->oDB->getOne($sSQL); - if ($iWordID === false) { - throw new \Exception('Query failed', 703); - } - if (!$iWordID) { - throw new \Exception('No value', 704); - } - } - - - public function normalizeString($sTerm) - { - if ($this->oNormalizer === null) { - return $sTerm; - } - - return $this->oNormalizer->transliterate($sTerm); - } - - - public function mostFrequentWords($iNum) - { - $sSQL = "SELECT word FROM word WHERE type = 'W'"; - $sSQL .= "ORDER BY info->'count' DESC LIMIT ".$iNum; - return $this->oDB->getCol($sSQL); - } - - - private function makeStandardWord($sTerm) - { - return trim($this->oTransliterator->transliterate(' '.$sTerm.' ')); - } - - - public function tokensForSpecialTerm($sTerm) - { - $aResults = array(); - - $sSQL = "SELECT word_id, info->>'class' as class, info->>'type' as type "; - $sSQL .= ' FROM word WHERE word_token = :term and type = \'S\''; - - Debug::printVar('Term', $sTerm); - Debug::printSQL($sSQL); - $aSearchWords = $this->oDB->getAll($sSQL, array(':term' => $this->makeStandardWord($sTerm))); - - Debug::printVar('Results', $aSearchWords); - - foreach ($aSearchWords as $aSearchTerm) { - $aResults[] = new \Nominatim\Token\SpecialTerm( - $aSearchTerm['word_id'], - $aSearchTerm['class'], - $aSearchTerm['type'], - \Nominatim\Operator::TYPE - ); - } - - Debug::printVar('Special term tokens', $aResults); - - return $aResults; - } - - - public function extractTokensFromPhrases(&$aPhrases) - { - $sNormQuery = ''; - $aWordLists = array(); - $aTokens = array(); - foreach ($aPhrases as $iPhrase => $oPhrase) { - $sNormQuery .= ','.$this->normalizeString($oPhrase->getPhrase()); - $sPhrase = $this->makeStandardWord($oPhrase->getPhrase()); - Debug::printVar('Phrase', $sPhrase); - - $oWordList = new SimpleWordList($sPhrase); - $aTokens = array_merge($aTokens, $oWordList->getTokens()); - $aWordLists[] = $oWordList; - } - - Debug::printVar('Tokens', $aTokens); - Debug::printVar('WordLists', $aWordLists); - - $oValidTokens = $this->computeValidTokens($aTokens, $sNormQuery); - - foreach ($aPhrases as $iPhrase => $oPhrase) { - $oPhrase->setWordSets($aWordLists[$iPhrase]->getWordSets($oValidTokens)); - } - - return $oValidTokens; - } - - - private function computeValidTokens($aTokens, $sNormQuery) - { - $oValidTokens = new TokenList(); - - if (!empty($aTokens)) { - $this->addTokensFromDB($oValidTokens, $aTokens, $sNormQuery); - - // Try more interpretations for Tokens that could not be matched. - foreach ($aTokens as $sToken) { - if ($sToken[0] != ' ' && !$oValidTokens->contains($sToken)) { - if (preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData)) { - // US ZIP+4 codes - merge in the 5-digit ZIP code - $oValidTokens->addToken( - $sToken, - new Token\Postcode(null, $aData[1], 'us') - ); - } elseif (preg_match('/^[0-9]+$/', $sToken)) { - // Unknown single word token with a number. - // Assume it is a house number. - $oValidTokens->addToken( - $sToken, - new Token\HouseNumber(null, trim($sToken)) - ); - } - } - } - } - - return $oValidTokens; - } - - - private function addTokensFromDB(&$oValidTokens, $aTokens, $sNormQuery) - { - // Check which tokens we have, get the ID numbers - $sSQL = 'SELECT word_id, word_token, type, word,'; - $sSQL .= " info->>'op' as operator,"; - $sSQL .= " info->>'class' as class, info->>'type' as ctype,"; - $sSQL .= " info->>'count' as count,"; - $sSQL .= " info->>'lookup' as lookup"; - $sSQL .= ' FROM word WHERE word_token in ('; - $sSQL .= join(',', $this->oDB->getDBQuotedList($aTokens)).')'; - - Debug::printSQL($sSQL); - - $aDBWords = $this->oDB->getAll($sSQL, null, 'Could not get word tokens.'); - - foreach ($aDBWords as $aWord) { - $iId = (int) $aWord['word_id']; - $sTok = $aWord['word_token']; - - switch ($aWord['type']) { - case 'C': // country name tokens - if ($aWord['word'] !== null) { - $oValidTokens->addToken( - $sTok, - new Token\Country($iId, $aWord['word']) - ); - } - break; - case 'H': // house number tokens - $sLookup = $aWord['lookup'] ?? $aWord['word_token']; - $oValidTokens->addToken($sTok, new Token\HouseNumber($iId, $sLookup)); - break; - case 'P': // postcode tokens - // Postcodes are not normalized, so they may have content - // that makes SQL injection possible. Reject postcodes - // that would need special escaping. - if ($aWord['word'] !== null - && pg_escape_string($aWord['word']) == $aWord['word'] - ) { - $iSplitPos = strpos($aWord['word'], '@'); - if ($iSplitPos === false) { - $sPostcode = $aWord['word']; - } else { - $sPostcode = substr($aWord['word'], 0, $iSplitPos); - } - - $oValidTokens->addToken( - $sTok, - new Token\Postcode($iId, $sPostcode, null) - ); - } - break; - case 'S': // tokens for classification terms (special phrases) - if ($aWord['class'] !== null && $aWord['ctype'] !== null) { - $oValidTokens->addToken($sTok, new Token\SpecialTerm( - $iId, - $aWord['class'], - $aWord['ctype'], - (isset($aWord['operator'])) ? Operator::NEAR : Operator::NONE - )); - } - break; - case 'W': // full-word tokens - $oValidTokens->addToken($sTok, new Token\Word( - $iId, - (int) $aWord['count'], - substr_count($aWord['word_token'], ' ') - )); - break; - case 'w': // partial word terms - $oValidTokens->addToken($sTok, new Token\Partial( - $iId, - $aWord['word_token'], - (int) $aWord['count'] - )); - break; - default: - break; - } - } - } -} diff --git a/lib-php/tokenizer/legacy_tokenizer.php b/lib-php/tokenizer/legacy_tokenizer.php deleted file mode 100644 index 6f3d2304..00000000 --- a/lib-php/tokenizer/legacy_tokenizer.php +++ /dev/null @@ -1,265 +0,0 @@ -oDB =& $oDB; - $this->oNormalizer = \Transliterator::createFromRules(CONST_Term_Normalization_Rules); - } - - public function checkStatus() - { - $sStandardWord = $this->oDB->getOne("SELECT make_standard_name('a')"); - if ($sStandardWord === false) { - throw new \Exception('Module failed', 701); - } - - if ($sStandardWord != 'a') { - throw new \Exception('Module call failed', 702); - } - - $sSQL = "SELECT word_id FROM word WHERE word_token IN (' a')"; - $iWordID = $this->oDB->getOne($sSQL); - if ($iWordID === false) { - throw new \Exception('Query failed', 703); - } - if (!$iWordID) { - throw new \Exception('No value', 704); - } - } - - - public function normalizeString($sTerm) - { - if ($this->oNormalizer === null) { - return $sTerm; - } - - return $this->oNormalizer->transliterate($sTerm); - } - - - public function mostFrequentWords($iNum) - { - $sSQL = 'SELECT word FROM word WHERE word is not null '; - $sSQL .= 'ORDER BY search_name_count DESC LIMIT '.$iNum; - return $this->oDB->getCol($sSQL); - } - - - public function tokensForSpecialTerm($sTerm) - { - $aResults = array(); - - $sSQL = 'SELECT word_id, class, type FROM word '; - $sSQL .= ' WHERE word_token = \' \' || make_standard_name(:term)'; - $sSQL .= ' AND class is not null AND class not in (\'place\')'; - - Debug::printVar('Term', $sTerm); - Debug::printSQL($sSQL); - $aSearchWords = $this->oDB->getAll($sSQL, array(':term' => $sTerm)); - - Debug::printVar('Results', $aSearchWords); - - foreach ($aSearchWords as $aSearchTerm) { - $aResults[] = new \Nominatim\Token\SpecialTerm( - $aSearchTerm['word_id'], - $aSearchTerm['class'], - $aSearchTerm['type'], - \Nominatim\Operator::TYPE - ); - } - - Debug::printVar('Special term tokens', $aResults); - - return $aResults; - } - - - public function extractTokensFromPhrases(&$aPhrases) - { - // First get the normalized version of all phrases - $sNormQuery = ''; - $sSQL = 'SELECT '; - $aParams = array(); - foreach ($aPhrases as $iPhrase => $oPhrase) { - $sNormQuery .= ','.$this->normalizeString($oPhrase->getPhrase()); - $sSQL .= 'make_standard_name(:' .$iPhrase.') as p'.$iPhrase.','; - $aParams[':'.$iPhrase] = $oPhrase->getPhrase(); - - // Conflicts between US state abbreviations and various words - // for 'the' in different languages - switch (strtolower($oPhrase->getPhrase())) { - case 'il': - $aParams[':'.$iPhrase] = 'illinois'; - break; - case 'al': - $aParams[':'.$iPhrase] = 'alabama'; - break; - case 'la': - $aParams[':'.$iPhrase] = 'louisiana'; - break; - default: - $aParams[':'.$iPhrase] = $oPhrase->getPhrase(); - break; - } - } - $sSQL = substr($sSQL, 0, -1); - - Debug::printSQL($sSQL); - Debug::printVar('SQL parameters', $aParams); - - $aNormPhrases = $this->oDB->getRow($sSQL, $aParams); - - Debug::printVar('SQL result', $aNormPhrases); - - // now compute all possible tokens - $aWordLists = array(); - $aTokens = array(); - foreach ($aNormPhrases as $sPhrase) { - $oWordList = new SimpleWordList($sPhrase); - - foreach ($oWordList->getTokens() as $sToken) { - $aTokens[' '.$sToken] = ' '.$sToken; - $aTokens[$sToken] = $sToken; - } - - $aWordLists[] = $oWordList; - } - - Debug::printVar('Tokens', $aTokens); - Debug::printVar('WordLists', $aWordLists); - - $oValidTokens = $this->computeValidTokens($aTokens, $sNormQuery); - - foreach ($aPhrases as $iPhrase => $oPhrase) { - $oPhrase->setWordSets($aWordLists[$iPhrase]->getWordSets($oValidTokens)); - } - - return $oValidTokens; - } - - - private function computeValidTokens($aTokens, $sNormQuery) - { - $oValidTokens = new TokenList(); - - if (!empty($aTokens)) { - $this->addTokensFromDB($oValidTokens, $aTokens, $sNormQuery); - - // Try more interpretations for Tokens that could not be matched. - foreach ($aTokens as $sToken) { - if ($sToken[0] != ' ' && !$oValidTokens->contains($sToken)) { - if (preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData)) { - // US ZIP+4 codes - merge in the 5-digit ZIP code - $oValidTokens->addToken( - $sToken, - new Token\Postcode(null, $aData[1], 'us') - ); - } elseif (preg_match('/^[0-9]+$/', $sToken)) { - // Unknown single word token with a number. - // Assume it is a house number. - $oValidTokens->addToken( - $sToken, - new Token\HouseNumber(null, trim($sToken)) - ); - } - } - } - } - - return $oValidTokens; - } - - - private function addTokensFromDB(&$oValidTokens, $aTokens, $sNormQuery) - { - // Check which tokens we have, get the ID numbers - $sSQL = 'SELECT word_id, word_token, word, class, type, country_code,'; - $sSQL .= ' operator, coalesce(search_name_count, 0) as count'; - $sSQL .= ' FROM word WHERE word_token in ('; - $sSQL .= join(',', $this->oDB->getDBQuotedList($aTokens)).')'; - - Debug::printSQL($sSQL); - - $aDBWords = $this->oDB->getAll($sSQL, null, 'Could not get word tokens.'); - - foreach ($aDBWords as $aWord) { - $oToken = null; - $iId = (int) $aWord['word_id']; - - if ($aWord['class']) { - // Special terms need to appear in their normalized form. - // (postcodes are not normalized in the word table) - $sNormWord = $this->normalizeString($aWord['word']); - if ($aWord['word'] && strpos($sNormQuery, $sNormWord) === false) { - continue; - } - - if ($aWord['class'] == 'place' && $aWord['type'] == 'house') { - $oToken = new Token\HouseNumber($iId, trim($aWord['word_token'])); - } elseif ($aWord['class'] == 'place' && $aWord['type'] == 'postcode') { - if ($aWord['word'] - && pg_escape_string($aWord['word']) == $aWord['word'] - ) { - $oToken = new Token\Postcode( - $iId, - $aWord['word'], - $aWord['country_code'] - ); - } - } else { - // near and in operator the same at the moment - $oToken = new Token\SpecialTerm( - $iId, - $aWord['class'], - $aWord['type'], - $aWord['operator'] ? Operator::NEAR : Operator::NONE - ); - } - } elseif ($aWord['country_code']) { - $oToken = new Token\Country($iId, $aWord['country_code']); - } elseif ($aWord['word_token'][0] == ' ') { - $oToken = new Token\Word( - $iId, - (int) $aWord['count'], - substr_count($aWord['word_token'], ' ') - ); - // For backward compatibility: ignore all partial tokens with more - // than one word. - } elseif (strpos($aWord['word_token'], ' ') === false) { - $oToken = new Token\Partial( - $iId, - $aWord['word_token'], - (int) $aWord['count'] - ); - } - - if ($oToken) { - // remove any leading spaces - if ($aWord['word_token'][0] == ' ') { - $oValidTokens->addToken(substr($aWord['word_token'], 1), $oToken); - } else { - $oValidTokens->addToken($aWord['word_token'], $oToken); - } - } - } - } -} diff --git a/lib-php/website/deletable.php b/lib-php/website/deletable.php deleted file mode 100644 index ffb202fd..00000000 --- a/lib-php/website/deletable.php +++ /dev/null @@ -1,36 +0,0 @@ -getSet('format', array('json'), 'json'); -set_exception_handler_by_format($sOutputFormat); - -$oDB = new Nominatim\DB(CONST_Database_DSN); -$oDB->connect(); - -$sSQL = 'select placex.place_id, country_code,'; -$sSQL .= " name->'name' as name, i.* from placex, import_polygon_delete i"; -$sSQL .= ' where placex.osm_id = i.osm_id and placex.osm_type = i.osm_type'; -$sSQL .= ' and placex.class = i.class and placex.type = i.type'; -$aPolygons = $oDB->getAll($sSQL, null, 'Could not get list of deleted OSM elements.'); - -if (CONST_Debug) { - var_dump($aPolygons); - exit; -} - -if ($sOutputFormat == 'json') { - javascript_renderData($aPolygons); -} diff --git a/lib-php/website/details.php b/lib-php/website/details.php deleted file mode 100644 index 98fb6ef7..00000000 --- a/lib-php/website/details.php +++ /dev/null @@ -1,263 +0,0 @@ -getSet('format', array('json'), 'json'); -set_exception_handler_by_format($sOutputFormat); - -$aLangPrefOrder = $oParams->getPreferredLanguages(); - -$sPlaceId = $oParams->getString('place_id'); -$sOsmType = $oParams->getSet('osmtype', array('N', 'W', 'R')); -$iOsmId = $oParams->getInt('osmid', 0); -$sClass = $oParams->getString('class'); - -$bIncludeKeywords = $oParams->getBool('keywords', false); -$bIncludeAddressDetails = $oParams->getBool('addressdetails', false); -$bIncludeLinkedPlaces = $oParams->getBool('linkedplaces', true); -$bIncludeHierarchy = $oParams->getBool('hierarchy', false); -$bGroupHierarchy = $oParams->getBool('group_hierarchy', false); -$bIncludePolygonAsGeoJSON = $oParams->getBool('polygon_geojson', false); - -$oDB = new Nominatim\DB(CONST_Database_DSN); -$oDB->connect(); - -$sLanguagePrefArraySQL = $oDB->getArraySQL($oDB->getDBQuotedList($aLangPrefOrder)); - -if ($sOsmType && $iOsmId !== 0) { - $sSQL = 'SELECT place_id FROM placex WHERE osm_type = :type AND osm_id = :id'; - $aSQLParams = array(':type' => $sOsmType, ':id' => $iOsmId); - // osm_type and osm_id are not unique enough - if ($sClass) { - $sSQL .= ' AND class= :class'; - $aSQLParams[':class'] = $sClass; - } - $sSQL .= ' ORDER BY class ASC'; - $sPlaceId = $oDB->getOne($sSQL, $aSQLParams); - - - // Nothing? Maybe it's an interpolation. - // XXX Simply returns the first parent street it finds. It should - // get a house number and get the right interpolation. - if (!$sPlaceId && $sOsmType == 'W' && (!$sClass || $sClass == 'place')) { - $sSQL = 'SELECT place_id FROM location_property_osmline' - .' WHERE osm_id = :id LIMIT 1'; - $sPlaceId = $oDB->getOne($sSQL, array(':id' => $iOsmId)); - } - - // Be nice about our error messages for broken geometry - - if (!$sPlaceId && $oDB->tableExists('import_polygon_error')) { - $sSQL = 'SELECT '; - $sSQL .= ' osm_type, '; - $sSQL .= ' osm_id, '; - $sSQL .= ' errormessage, '; - $sSQL .= ' class, '; - $sSQL .= ' type, '; - $sSQL .= " get_name_by_language(name,$sLanguagePrefArraySQL) AS localname,"; - $sSQL .= ' ST_AsText(prevgeometry) AS prevgeom, '; - $sSQL .= ' ST_AsText(newgeometry) AS newgeom'; - $sSQL .= ' FROM import_polygon_error '; - $sSQL .= ' WHERE osm_type = :type'; - $sSQL .= ' AND osm_id = :id'; - $sSQL .= ' ORDER BY updated DESC'; - $sSQL .= ' LIMIT 1'; - $aPointDetails = $oDB->getRow($sSQL, array(':type' => $sOsmType, ':id' => $iOsmId)); - if ($aPointDetails) { - if (preg_match('/\[(-?\d+\.\d+) (-?\d+\.\d+)\]/', $aPointDetails['errormessage'], $aMatches)) { - $aPointDetails['error_x'] = $aMatches[1]; - $aPointDetails['error_y'] = $aMatches[2]; - } else { - $aPointDetails['error_x'] = 0; - $aPointDetails['error_y'] = 0; - } - include(CONST_LibDir.'/template/details-error-'.$sOutputFormat.'.php'); - exit; - } - } - - if ($sPlaceId === false) { - throw new \Exception('No place with that OSM ID found.', 404); - } -} else { - if ($sPlaceId === false) { - userError('Required parameters missing. Need either osmtype/osmid or place_id.'); - } -} - -$iPlaceID = (int)$sPlaceId; - -if (CONST_Use_US_Tiger_Data) { - $iParentPlaceID = $oDB->getOne('SELECT parent_place_id FROM location_property_tiger WHERE place_id = '.$iPlaceID); - if ($iParentPlaceID) { - $iPlaceID = $iParentPlaceID; - } -} - -// interpolated house numbers -$iParentPlaceID = $oDB->getOne('SELECT parent_place_id FROM location_property_osmline WHERE place_id = '.$iPlaceID); -if ($iParentPlaceID) { - $iPlaceID = $iParentPlaceID; -} - -// artificial postcodes -$iParentPlaceID = $oDB->getOne('SELECT parent_place_id FROM location_postcode WHERE place_id = '.$iPlaceID); -if ($iParentPlaceID) { - $iPlaceID = $iParentPlaceID; -} - -$hLog = logStart($oDB, 'details', $_SERVER['QUERY_STRING'], $aLangPrefOrder); - -// Get the details for this point -$sSQL = 'SELECT place_id, osm_type, osm_id, class, type, name, admin_level,'; -$sSQL .= ' housenumber, postcode, country_code,'; -$sSQL .= ' importance, wikipedia,'; -$sSQL .= ' ROUND(EXTRACT(epoch FROM indexed_date)) AS indexed_epoch,'; -$sSQL .= ' parent_place_id, '; -$sSQL .= ' rank_address, '; -$sSQL .= ' rank_search, '; -$sSQL .= " get_name_by_language(name,$sLanguagePrefArraySQL) AS localname, "; -$sSQL .= " ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon') AS isarea, "; -$sSQL .= ' ST_y(centroid) AS lat, '; -$sSQL .= ' ST_x(centroid) AS lon, '; -$sSQL .= ' CASE '; -$sSQL .= ' WHEN importance = 0 OR importance IS NULL '; -$sSQL .= ' THEN 0.75-(rank_search::float/40) '; -$sSQL .= ' ELSE importance '; -$sSQL .= ' END as calculated_importance, '; -if ($bIncludePolygonAsGeoJSON) { - $sSQL .= ' ST_AsGeoJSON(CASE '; - $sSQL .= ' WHEN ST_NPoints(geometry) > 5000 '; - $sSQL .= ' THEN ST_SimplifyPreserveTopology(geometry, 0.0001) '; - $sSQL .= ' ELSE geometry '; - $sSQL .= ' END) as asgeojson'; -} else { - $sSQL .= ' ST_AsGeoJSON(centroid) as asgeojson'; -} -$sSQL .= ' FROM placex '; -$sSQL .= " WHERE place_id = $iPlaceID"; - -$aPointDetails = $oDB->getRow($sSQL, null, 'Could not get details of place object.'); - -if (!$aPointDetails) { - throw new \Exception('No place with that place ID found.', 404); -} - -$aPointDetails['localname'] = $aPointDetails['localname']?$aPointDetails['localname']:$aPointDetails['housenumber']; - -// Get all alternative names (languages, etc) -$sSQL = 'SELECT (each(name)).key,(each(name)).value FROM placex '; -$sSQL .= "WHERE place_id = $iPlaceID ORDER BY (each(name)).key"; -$aPointDetails['aNames'] = $oDB->getAssoc($sSQL); - -// Address tags -$sSQL = 'SELECT (each(address)).key as key,(each(address)).value FROM placex '; -$sSQL .= "WHERE place_id = $iPlaceID ORDER BY key"; -$aPointDetails['aAddressTags'] = $oDB->getAssoc($sSQL); - -// Extra tags -$sSQL = 'SELECT (each(extratags)).key,(each(extratags)).value FROM placex '; -$sSQL .= "WHERE place_id = $iPlaceID ORDER BY (each(extratags)).key"; -$aPointDetails['aExtraTags'] = $oDB->getAssoc($sSQL); - -// Address -$aAddressLines = false; -if ($bIncludeAddressDetails) { - $oDetails = new Nominatim\AddressDetails($oDB, $iPlaceID, -1, $sLanguagePrefArraySQL); - $aAddressLines = $oDetails->getAddressDetails(true); -} - -// Linked places -$aLinkedLines = false; -if ($bIncludeLinkedPlaces) { - $sSQL = 'SELECT placex.place_id, osm_type, osm_id, class, type, housenumber,'; - $sSQL .= ' admin_level, rank_address, '; - $sSQL .= " ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon') AS isarea,"; - $sSQL .= " ST_DistanceSpheroid(geometry, placegeometry, 'SPHEROID[\"WGS 84\",6378137,298.257223563, AUTHORITY[\"EPSG\",\"7030\"]]') AS distance, "; - $sSQL .= " get_name_by_language(name,$sLanguagePrefArraySQL) AS localname, "; - $sSQL .= ' length(name::text) AS namelength '; - $sSQL .= ' FROM '; - $sSQL .= ' placex, '; - $sSQL .= ' ( '; - $sSQL .= ' SELECT centroid AS placegeometry '; - $sSQL .= ' FROM placex '; - $sSQL .= " WHERE place_id = $iPlaceID "; - $sSQL .= ' ) AS x'; - $sSQL .= " WHERE linked_place_id = $iPlaceID"; - $sSQL .= ' ORDER BY '; - $sSQL .= ' rank_address ASC, '; - $sSQL .= ' rank_search ASC, '; - $sSQL .= " get_name_by_language(name, $sLanguagePrefArraySQL), "; - $sSQL .= ' housenumber'; - $aLinkedLines = $oDB->getAll($sSQL); -} - -// All places this is an immediate parent of -$aHierarchyLines = false; -if ($bIncludeHierarchy) { - $sSQL = 'SELECT obj.place_id, osm_type, osm_id, class, type, housenumber,'; - $sSQL .= " admin_level, rank_address, ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon') AS isarea,"; - $sSQL .= " ST_DistanceSpheroid(geometry, placegeometry, 'SPHEROID[\"WGS 84\",6378137,298.257223563, AUTHORITY[\"EPSG\",\"7030\"]]') AS distance, "; - $sSQL .= " get_name_by_language(name,$sLanguagePrefArraySQL) AS localname, "; - $sSQL .= ' length(name::text) AS namelength '; - $sSQL .= ' FROM '; - $sSQL .= ' ( '; - $sSQL .= ' SELECT placex.place_id, osm_type, osm_id, class, type, housenumber, admin_level, rank_address, rank_search, geometry, name '; - $sSQL .= ' FROM placex '; - $sSQL .= " WHERE parent_place_id = $iPlaceID "; - $sSQL .= ' ORDER BY '; - $sSQL .= ' rank_address ASC, '; - $sSQL .= ' rank_search ASC '; - $sSQL .= ' LIMIT 500 '; - $sSQL .= ' ) AS obj,'; - $sSQL .= ' ( '; - $sSQL .= ' SELECT centroid AS placegeometry '; - $sSQL .= ' FROM placex '; - $sSQL .= " WHERE place_id = $iPlaceID "; - $sSQL .= ' ) AS x'; - $sSQL .= ' ORDER BY '; - $sSQL .= ' rank_address ASC, '; - $sSQL .= ' rank_search ASC, '; - $sSQL .= ' localname, '; - $sSQL .= ' housenumber'; - $aHierarchyLines = $oDB->getAll($sSQL); -} - -$aPlaceSearchNameKeywords = false; -$aPlaceSearchAddressKeywords = false; -if ($bIncludeKeywords) { - $sSQL = "SELECT * FROM search_name WHERE place_id = $iPlaceID"; - $aPlaceSearchName = $oDB->getRow($sSQL); - - if (!empty($aPlaceSearchName)) { - $sWordIds = substr($aPlaceSearchName['name_vector'], 1, -1); - if (!empty($sWordIds)) { - $sSQL = 'SELECT * FROM word WHERE word_id in ('.$sWordIds.')'; - $aPlaceSearchNameKeywords = $oDB->getAll($sSQL); - } - - $sWordIds = substr($aPlaceSearchName['nameaddress_vector'], 1, -1); - if (!empty($sWordIds)) { - $sSQL = 'SELECT * FROM word WHERE word_id in ('.$sWordIds.')'; - $aPlaceSearchAddressKeywords = $oDB->getAll($sSQL); - } - } -} - -logEnd($oDB, $hLog, 1); - -include(CONST_LibDir.'/template/details-'.$sOutputFormat.'.php'); diff --git a/lib-php/website/lookup.php b/lib-php/website/lookup.php deleted file mode 100644 index 3a7ddb85..00000000 --- a/lib-php/website/lookup.php +++ /dev/null @@ -1,101 +0,0 @@ -getSet('format', array('xml', 'json', 'jsonv2', 'geojson', 'geocodejson'), 'xml'); -set_exception_handler_by_format($sOutputFormat); - -// Preferred language -$aLangPrefOrder = $oParams->getPreferredLanguages(); - -$oDB = new Nominatim\DB(CONST_Database_DSN); -$oDB->connect(); - -$hLog = logStart($oDB, 'place', $_SERVER['QUERY_STRING'], $aLangPrefOrder); - -$aSearchResults = array(); -$aCleanedQueryParts = array(); - -$oPlaceLookup = new Nominatim\PlaceLookup($oDB); -$oPlaceLookup->loadParamArray($oParams); -$oPlaceLookup->setIncludeAddressDetails($oParams->getBool('addressdetails', true)); - -$aOsmIds = explode(',', $oParams->getString('osm_ids', '')); - -if (count($aOsmIds) > CONST_Places_Max_ID_count) { - userError('Bulk User: Only ' . CONST_Places_Max_ID_count . ' ids are allowed in one request.'); -} - -foreach ($aOsmIds as $sItem) { - // Skip empty sItem - if (empty($sItem)) { - continue; - } - - $sType = $sItem[0]; - $iId = (int) substr($sItem, 1); - if ($iId > 0 && ($sType == 'N' || $sType == 'W' || $sType == 'R')) { - $aCleanedQueryParts[] = $sType . $iId; - $oPlace = $oPlaceLookup->lookupOSMID($sType, $iId); - if ($oPlace) { - // we want to use the search-* output templates, so we need to fill - // $aSearchResults and slightly change the (reverse search) oPlace - // key names - $oResult = $oPlace; - unset($oResult['aAddress']); - if (isset($oPlace['aAddress'])) { - $oResult['address'] = $oPlace['aAddress']; - } - if ($sOutputFormat != 'geocodejson') { - unset($oResult['langaddress']); - $oResult['name'] = $oPlace['langaddress']; - } - - $aOutlineResult = $oPlaceLookup->getOutlines( - $oPlace['place_id'], - $oPlace['lon'], - $oPlace['lat'], - Nominatim\ClassTypes\getDefRadius($oPlace) - ); - - if ($aOutlineResult) { - $oResult = array_merge($oResult, $aOutlineResult); - } - - $aSearchResults[] = $oResult; - } - } -} - - -if (CONST_Debug) { - exit; -} - -$sXmlRootTag = 'lookupresults'; -$sQuery = join(',', $aCleanedQueryParts); -// we initialize these to avoid warnings in our logfile -$sViewBox = ''; -$bShowPolygons = ''; -$aExcludePlaceIDs = array(); -$sMoreURL = ''; - -logEnd($oDB, $hLog, 1); - -$sOutputTemplate = ($sOutputFormat == 'jsonv2') ? 'json' : $sOutputFormat; -include(CONST_LibDir.'/template/search-'.$sOutputTemplate.'.php'); diff --git a/lib-php/website/polygons.php b/lib-php/website/polygons.php deleted file mode 100644 index 5a90abe5..00000000 --- a/lib-php/website/polygons.php +++ /dev/null @@ -1,63 +0,0 @@ -getSet('format', array('json'), 'json'); -set_exception_handler_by_format($sOutputFormat); - -$iDays = $oParams->getInt('days', false); -$bReduced = $oParams->getBool('reduced', false); -$sClass = $oParams->getString('class', false); - -$oDB = new Nominatim\DB(CONST_Database_DSN); -$oDB->connect(); - -$iTotalBroken = (int) $oDB->getOne('SELECT count(*) FROM import_polygon_error'); - -$aPolygons = array(); -while ($iTotalBroken && empty($aPolygons)) { - $sSQL = 'SELECT osm_type, osm_id, class, type, name->\'name\' as "name",'; - $sSQL .= 'country_code, errormessage, updated'; - $sSQL .= ' FROM import_polygon_error'; - - $aWhere = array(); - if ($iDays) { - $aWhere[] = "updated > 'now'::timestamp - '".$iDays." day'::interval"; - $iDays++; - } - - if ($bReduced) { - $aWhere[] = "errormessage like 'Area reduced%'"; - } - if ($sClass) { - $sWhere[] = "class = '".pg_escape_string($sClass)."'"; - } - - if (!empty($aWhere)) { - $sSQL .= ' WHERE '.join(' and ', $aWhere); - } - - $sSQL .= ' ORDER BY updated desc LIMIT 1000'; - $aPolygons = $oDB->getAll($sSQL); -} - -if (CONST_Debug) { - var_dump($aPolygons); - exit; -} - -if ($sOutputFormat == 'json') { - javascript_renderData($aPolygons); -} diff --git a/lib-php/website/reverse-only-search.php b/lib-php/website/reverse-only-search.php deleted file mode 100644 index 43cbd265..00000000 --- a/lib-php/website/reverse-only-search.php +++ /dev/null @@ -1,20 +0,0 @@ -getSet('format', array('xml', 'json', 'jsonv2', 'geojson', 'geocodejson'), 'jsonv2'); -set_exception_handler_by_format($sOutputFormat); - -throw new Exception('Reverse-only import does not support forward searching.', 404); diff --git a/lib-php/website/reverse.php b/lib-php/website/reverse.php deleted file mode 100644 index f24c655a..00000000 --- a/lib-php/website/reverse.php +++ /dev/null @@ -1,95 +0,0 @@ -getSet('format', array('xml', 'json', 'jsonv2', 'geojson', 'geocodejson'), 'xml'); -set_exception_handler_by_format($sOutputFormat); - -// Preferred language -$aLangPrefOrder = $oParams->getPreferredLanguages(); - -$oDB = new Nominatim\DB(CONST_Database_DSN); -$oDB->connect(); - -$hLog = logStart($oDB, 'reverse', $_SERVER['QUERY_STRING'], $aLangPrefOrder); - -$oPlaceLookup = new Nominatim\PlaceLookup($oDB); -$oPlaceLookup->loadParamArray($oParams); -$oPlaceLookup->setIncludeAddressDetails($oParams->getBool('addressdetails', true)); - -$sOsmType = $oParams->getSet('osm_type', array('N', 'W', 'R')); -$iOsmId = $oParams->getInt('osm_id', -1); -$fLat = $oParams->getFloat('lat'); -$fLon = $oParams->getFloat('lon'); -$iZoom = $oParams->getInt('zoom', 18); - -if ($sOsmType && $iOsmId > 0) { - $aPlace = $oPlaceLookup->lookupOSMID($sOsmType, $iOsmId); -} elseif ($fLat !== false && $fLon !== false) { - $oReverseGeocode = new Nominatim\ReverseGeocode($oDB); - $oReverseGeocode->setZoom($iZoom); - - $oLookup = $oReverseGeocode->lookup($fLat, $fLon); - - if ($oLookup) { - $aPlaces = $oPlaceLookup->lookup(array($oLookup->iId => $oLookup)); - if (!empty($aPlaces)) { - $aPlace = reset($aPlaces); - } - } -} else { - userError('Need coordinates or OSM object to lookup.'); -} - -if (isset($aPlace)) { - $aOutlineResult = $oPlaceLookup->getOutlines( - $aPlace['place_id'], - $aPlace['lon'], - $aPlace['lat'], - Nominatim\ClassTypes\getDefRadius($aPlace), - $fLat, - $fLon - ); - - if ($aOutlineResult) { - $aPlace = array_merge($aPlace, $aOutlineResult); - } -} else { - $aPlace = array(); -} - -logEnd($oDB, $hLog, count($aPlace) ? 1 : 0); - -if (CONST_Debug) { - var_dump($aPlace); - exit; -} - -if ($sOutputFormat == 'geocodejson') { - $sQuery = $fLat.','.$fLon; - if (isset($aPlace['place_id'])) { - $fDistance = $oDB->getOne( - 'SELECT ST_Distance(ST_SetSRID(ST_Point(:lon,:lat),4326), centroid) FROM placex where place_id = :placeid', - array(':lon' => $fLon, ':lat' => $fLat, ':placeid' => $aPlace['place_id']) - ); - } -} - -$sOutputTemplate = ($sOutputFormat == 'jsonv2') ? 'json' : $sOutputFormat; -include(CONST_LibDir.'/template/address-'.$sOutputTemplate.'.php'); diff --git a/lib-php/website/search.php b/lib-php/website/search.php deleted file mode 100644 index e1aab794..00000000 --- a/lib-php/website/search.php +++ /dev/null @@ -1,98 +0,0 @@ -connect(); -$oParams = new Nominatim\ParameterParser(); - -$oGeocode = new Nominatim\Geocode($oDB); - -$aLangPrefOrder = $oParams->getPreferredLanguages(); -$oGeocode->setLanguagePreference($aLangPrefOrder); - -// Format for output -$sOutputFormat = $oParams->getSet('format', array('xml', 'json', 'jsonv2', 'geojson', 'geocodejson'), 'jsonv2'); -set_exception_handler_by_format($sOutputFormat); - -$oGeocode->loadParamArray($oParams, null); - -if (CONST_Search_BatchMode && isset($_GET['batch'])) { - $aBatch = json_decode($_GET['batch'], true); - $aBatchResults = array(); - foreach ($aBatch as $aBatchParams) { - $oBatchGeocode = clone $oGeocode; - $oBatchParams = new Nominatim\ParameterParser($aBatchParams); - $oBatchGeocode->loadParamArray($oBatchParams); - $oBatchGeocode->setQueryFromParams($oBatchParams); - $aSearchResults = $oBatchGeocode->lookup(); - $aBatchResults[] = $aSearchResults; - } - include(CONST_LibDir.'/template/search-batch-json.php'); - exit; -} - -$oGeocode->setQueryFromParams($oParams); - -if (!$oGeocode->getQueryString() - && isset($_SERVER['PATH_INFO']) - && strlen($_SERVER['PATH_INFO']) > 0 - && $_SERVER['PATH_INFO'][0] == '/' -) { - $sQuery = substr(rawurldecode($_SERVER['PATH_INFO']), 1); - - // reverse order of '/' separated string - $aPhrases = explode('/', $sQuery); - $aPhrases = array_reverse($aPhrases); - $sQuery = join(', ', $aPhrases); - $oGeocode->setQuery($sQuery); -} - -$hLog = logStart($oDB, 'search', $oGeocode->getQueryString(), $aLangPrefOrder); - -// Ignore requests from a faulty app. -if ($oGeocode->getQueryString() === '-1,-1') { - $aSearchResults = array(); -} else { - $aSearchResults = $oGeocode->lookup(); -} - -logEnd($oDB, $hLog, count($aSearchResults)); - -$sQuery = $oGeocode->getQueryString(); - -$aMoreParams = $oGeocode->getMoreUrlParams(); -$aMoreParams['format'] = $sOutputFormat; -if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { - $aMoreParams['accept-language'] = $_SERVER['HTTP_ACCEPT_LANGUAGE']; -} - -if (isset($_SERVER['REQUEST_SCHEME']) - && isset($_SERVER['HTTP_HOST']) - && isset($_SERVER['DOCUMENT_URI']) -) { - $sMoreURL = $_SERVER['REQUEST_SCHEME'].'://' - .$_SERVER['HTTP_HOST'].$_SERVER['DOCUMENT_URI'].'/?' - .http_build_query($aMoreParams); -} else { - $sMoreURL = '/search.php?'.http_build_query($aMoreParams); -} - -if (CONST_Debug) { - exit; -} - -$sOutputTemplate = ($sOutputFormat == 'jsonv2') ? 'json' : $sOutputFormat; -include(CONST_LibDir.'/template/search-'.$sOutputTemplate.'.php'); diff --git a/lib-php/website/status.php b/lib-php/website/status.php deleted file mode 100644 index 8ab11cc4..00000000 --- a/lib-php/website/status.php +++ /dev/null @@ -1,58 +0,0 @@ -getSet('format', array('text', 'json'), 'text'); - -$oDB = new Nominatim\DB(CONST_Database_DSN); - -if ($sOutputFormat == 'json') { - header('content-type: application/json; charset=UTF-8'); -} - - -try { - $oStatus = new Nominatim\Status($oDB); - $oStatus->status(); - - if ($sOutputFormat == 'json') { - $epoch = $oStatus->dataDate(); - $aResponse = array( - 'status' => 0, - 'message' => 'OK', - 'server' => gethostname(), - 'data_updated' => (new DateTime('@'.$epoch))->format(DateTime::RFC3339), - 'software_version' => CONST_NominatimVersion - ); - $sDatabaseVersion = $oStatus->databaseVersion(); - if ($sDatabaseVersion) { - $aResponse['database_version'] = $sDatabaseVersion; - } - javascript_renderData($aResponse); - } else { - echo 'OK'; - } -} catch (Exception $oErr) { - if ($sOutputFormat == 'json') { - $aResponse = array( - 'status' => $oErr->getCode(), - 'message' => $oErr->getMessage(), - 'server' => gethostname() - ); - javascript_renderData($aResponse); - } else { - header('HTTP/1.0 500 Internal Server Error'); - echo 'ERROR: '.$oErr->getMessage(); - } -} diff --git a/lib-sql/tokenizer/legacy_tokenizer.sql b/lib-sql/tokenizer/legacy_tokenizer.sql deleted file mode 100644 index c21d0510..00000000 --- a/lib-sql/tokenizer/legacy_tokenizer.sql +++ /dev/null @@ -1,426 +0,0 @@ --- SPDX-License-Identifier: GPL-2.0-only --- --- This file is part of Nominatim. (https://nominatim.org) --- --- Copyright (C) 2022 by the Nominatim developer community. --- For a full list of authors see the git log. - --- Get tokens used for searching the given place. --- --- These are the tokens that will be saved in the search_name table. -CREATE OR REPLACE FUNCTION token_get_name_search_tokens(info JSONB) - RETURNS INTEGER[] -AS $$ - SELECT (info->>'names')::INTEGER[] -$$ LANGUAGE SQL IMMUTABLE STRICT; - - --- Get tokens for matching the place name against others. --- --- This should usually be restricted to full name tokens. -CREATE OR REPLACE FUNCTION token_get_name_match_tokens(info JSONB) - RETURNS INTEGER[] -AS $$ - SELECT (info->>'names')::INTEGER[] -$$ LANGUAGE SQL IMMUTABLE STRICT; - - --- Return the housenumber tokens applicable for the place. -CREATE OR REPLACE FUNCTION token_get_housenumber_search_tokens(info JSONB) - RETURNS INTEGER[] -AS $$ - SELECT (info->>'hnr_tokens')::INTEGER[] -$$ LANGUAGE SQL IMMUTABLE STRICT; - - --- Return the housenumber in the form that it can be matched during search. -CREATE OR REPLACE FUNCTION token_normalized_housenumber(info JSONB) - RETURNS TEXT -AS $$ - SELECT info->>'hnr'; -$$ LANGUAGE SQL IMMUTABLE STRICT; - - -CREATE OR REPLACE FUNCTION token_is_street_address(info JSONB) - RETURNS BOOLEAN -AS $$ - SELECT info->>'street' is not null or info->>'place_search' is null; -$$ LANGUAGE SQL IMMUTABLE; - - -CREATE OR REPLACE FUNCTION token_has_addr_street(info JSONB) - RETURNS BOOLEAN -AS $$ - SELECT info->>'street' is not null and info->>'street' != '{}'; -$$ LANGUAGE SQL IMMUTABLE; - - -CREATE OR REPLACE FUNCTION token_has_addr_place(info JSONB) - RETURNS BOOLEAN -AS $$ - SELECT info->>'place_match' is not null; -$$ LANGUAGE SQL IMMUTABLE; - - -CREATE OR REPLACE FUNCTION token_matches_street(info JSONB, street_tokens INTEGER[]) - RETURNS BOOLEAN -AS $$ - SELECT (info->>'street')::INTEGER[] && street_tokens -$$ LANGUAGE SQL IMMUTABLE STRICT; - - -CREATE OR REPLACE FUNCTION token_matches_place(info JSONB, place_tokens INTEGER[]) - RETURNS BOOLEAN -AS $$ - SELECT (info->>'place_match')::INTEGER[] && place_tokens -$$ LANGUAGE SQL IMMUTABLE STRICT; - - -CREATE OR REPLACE FUNCTION token_addr_place_search_tokens(info JSONB) - RETURNS INTEGER[] -AS $$ - SELECT (info->>'place_search')::INTEGER[] -$$ LANGUAGE SQL IMMUTABLE STRICT; - - -CREATE OR REPLACE FUNCTION token_get_address_keys(info JSONB) - RETURNS SETOF TEXT -AS $$ - SELECT * FROM jsonb_object_keys(info->'addr'); -$$ LANGUAGE SQL IMMUTABLE STRICT; - - -CREATE OR REPLACE FUNCTION token_get_address_search_tokens(info JSONB, key TEXT) - RETURNS INTEGER[] -AS $$ - SELECT (info->'addr'->key->>0)::INTEGER[]; -$$ LANGUAGE SQL IMMUTABLE STRICT; - - -CREATE OR REPLACE FUNCTION token_matches_address(info JSONB, key TEXT, tokens INTEGER[]) - RETURNS BOOLEAN -AS $$ - SELECT (info->'addr'->key->>1)::INTEGER[] && tokens; -$$ LANGUAGE SQL IMMUTABLE STRICT; - - -CREATE OR REPLACE FUNCTION token_get_postcode(info JSONB) - RETURNS TEXT -AS $$ - SELECT info->>'postcode'; -$$ LANGUAGE SQL IMMUTABLE STRICT; - - --- Return token info that should be saved permanently in the database. -CREATE OR REPLACE FUNCTION token_strip_info(info JSONB) - RETURNS JSONB -AS $$ - SELECT NULL::JSONB; -$$ LANGUAGE SQL IMMUTABLE STRICT; - ---------------- private functions ---------------------------------------------- - --- Functions for term normalisation and access to the 'word' table. - -CREATE OR REPLACE FUNCTION transliteration(text) RETURNS text - AS '{{ modulepath }}/nominatim.so', 'transliteration' -LANGUAGE c IMMUTABLE STRICT; - - -CREATE OR REPLACE FUNCTION gettokenstring(text) RETURNS text - AS '{{ modulepath }}/nominatim.so', 'gettokenstring' -LANGUAGE c IMMUTABLE STRICT; - - -CREATE OR REPLACE FUNCTION make_standard_name(name TEXT) RETURNS TEXT - AS $$ -DECLARE - o TEXT; -BEGIN - o := public.gettokenstring(public.transliteration(name)); - RETURN trim(substr(o,1,length(o))); -END; -$$ -LANGUAGE plpgsql IMMUTABLE; - --- returns NULL if the word is too common -CREATE OR REPLACE FUNCTION getorcreate_word_id(lookup_word TEXT) - RETURNS INTEGER - AS $$ -DECLARE - lookup_token TEXT; - return_word_id INTEGER; - count INTEGER; -BEGIN - lookup_token := trim(lookup_word); - SELECT min(word_id), max(search_name_count) FROM word - WHERE word_token = lookup_token and class is null and type is null - INTO return_word_id, count; - IF return_word_id IS NULL THEN - return_word_id := nextval('seq_word'); - INSERT INTO word VALUES (return_word_id, lookup_token, null, null, null, null, 0); - ELSE - IF count > {{ max_word_freq }} THEN - return_word_id := NULL; - END IF; - END IF; - RETURN return_word_id; -END; -$$ -LANGUAGE plpgsql; - - --- Create housenumber tokens from an OSM addr:housenumber. --- The housnumber is split at comma and semicolon as necessary. --- The function returns the normalized form of the housenumber suitable --- for comparison. -CREATE OR REPLACE FUNCTION create_housenumbers(housenumbers TEXT[], - OUT tokens TEXT, - OUT normtext TEXT) - AS $$ -BEGIN - SELECT array_to_string(array_agg(trans), ';'), array_agg(tid)::TEXT - INTO normtext, tokens - FROM (SELECT lookup_word as trans, getorcreate_housenumber_id(lookup_word) as tid - FROM (SELECT make_standard_name(h) as lookup_word - FROM unnest(housenumbers) h) x) y; -END; -$$ LANGUAGE plpgsql STABLE STRICT; - - -CREATE OR REPLACE FUNCTION getorcreate_housenumber_id(lookup_word 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 class='place' and type='house' - 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, null, - 'place', 'house', null, 0); - END IF; - RETURN return_word_id; -END; -$$ -LANGUAGE plpgsql; - - -CREATE OR REPLACE FUNCTION create_postcode_id(postcode TEXT) - RETURNS BOOLEAN - AS $$ -DECLARE - r RECORD; - lookup_token TEXT; - return_word_id INTEGER; -BEGIN - lookup_token := ' ' || make_standard_name(postcode); - FOR r IN - SELECT word_id FROM word - WHERE word_token = lookup_token and word = postcode - and class='place' and type='postcode' - LOOP - RETURN false; - END LOOP; - - INSERT INTO word VALUES (nextval('seq_word'), lookup_token, postcode, - 'place', 'postcode', null, 0); - RETURN true; -END; -$$ -LANGUAGE plpgsql; - - -CREATE OR REPLACE FUNCTION getorcreate_name_id(lookup_word TEXT, src_word TEXT) - RETURNS INTEGER - AS $$ -DECLARE - lookup_token TEXT; - nospace_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 class is null and type is null - 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, src_word, - null, null, null, 0); - END IF; - RETURN return_word_id; -END; -$$ -LANGUAGE plpgsql; - - --- Normalize a string and lookup its word ids (partial words). -CREATE OR REPLACE FUNCTION addr_ids_from_name(lookup_word TEXT) - RETURNS INTEGER[] - AS $$ -DECLARE - words TEXT[]; - id INTEGER; - return_word_id INTEGER[]; - word_ids INTEGER[]; - j INTEGER; -BEGIN - words := string_to_array(make_standard_name(lookup_word), ' '); - IF array_upper(words, 1) IS NOT NULL THEN - FOR j IN 1..array_upper(words, 1) LOOP - IF (words[j] != '') THEN - SELECT array_agg(word_id) INTO word_ids - FROM word - WHERE word_token = words[j] and class is null and type is null; - - IF word_ids IS NULL THEN - id := nextval('seq_word'); - INSERT INTO word VALUES (id, words[j], null, null, null, null, 0); - return_word_id := return_word_id || id; - ELSE - return_word_id := array_merge(return_word_id, word_ids); - END IF; - END IF; - END LOOP; - END IF; - - RETURN return_word_id; -END; -$$ -LANGUAGE plpgsql; - - --- Normalize a string and look up its name ids (full words). -CREATE OR REPLACE FUNCTION word_ids_from_name(lookup_word TEXT) - RETURNS INTEGER[] - AS $$ -DECLARE - lookup_token TEXT; - return_word_ids INTEGER[]; -BEGIN - lookup_token := ' '|| make_standard_name(lookup_word); - SELECT array_agg(word_id) FROM word - WHERE word_token = lookup_token and class is null and type is null - INTO return_word_ids; - RETURN return_word_ids; -END; -$$ -LANGUAGE plpgsql STABLE STRICT; - - -CREATE OR REPLACE FUNCTION make_keywords(src HSTORE) - RETURNS INTEGER[] - AS $$ -DECLARE - result INTEGER[]; - s TEXT; - w INTEGER; - words TEXT[]; - value TEXT; - j INTEGER; -BEGIN - result := '{}'::INTEGER[]; - - FOR value IN SELECT unnest(regexp_split_to_array(svals(src), E'[,;]')) LOOP - -- full name - s := make_standard_name(value); - w := getorcreate_name_id(s, value); - - IF not(ARRAY[w] <@ result) THEN - result := result || w; - END IF; - - -- partial single-word terms - words := string_to_array(s, ' '); - IF array_upper(words, 1) IS NOT NULL THEN - FOR j IN 1..array_upper(words, 1) LOOP - IF (words[j] != '') THEN - w = getorcreate_word_id(words[j]); - IF w IS NOT NULL AND NOT (ARRAY[w] <@ result) THEN - result := result || w; - END IF; - END IF; - END LOOP; - END IF; - - -- consider parts before an opening bracket a full word as well - words := regexp_split_to_array(value, E'[(]'); - IF array_upper(words, 1) > 1 THEN - s := make_standard_name(words[1]); - IF s != '' THEN - w := getorcreate_name_id(s, words[1]); - IF w IS NOT NULL AND NOT (ARRAY[w] <@ result) THEN - result := result || w; - END IF; - END IF; - END IF; - - s := regexp_replace(value, '市$', ''); - IF s != value THEN - s := make_standard_name(s); - IF s != '' THEN - w := getorcreate_name_id(s, value); - IF NOT (ARRAY[w] <@ result) THEN - result := result || w; - END IF; - END IF; - END IF; - - END LOOP; - - RETURN result; -END; -$$ -LANGUAGE plpgsql; - - -CREATE OR REPLACE FUNCTION precompute_words(src TEXT) - RETURNS INTEGER - AS $$ -DECLARE - s TEXT; - w INTEGER; - words TEXT[]; - i INTEGER; - j INTEGER; -BEGIN - s := make_standard_name(src); - w := getorcreate_name_id(s, src); - - w := getorcreate_word_id(s); - - words := string_to_array(s, ' '); - IF array_upper(words, 1) IS NOT NULL THEN - FOR j IN 1..array_upper(words, 1) LOOP - IF (words[j] != '') THEN - w := getorcreate_word_id(words[j]); - END IF; - END LOOP; - END IF; - - words := regexp_split_to_array(src, E'[,;()]'); - IF array_upper(words, 1) != 1 THEN - FOR j IN 1..array_upper(words, 1) LOOP - s := make_standard_name(words[j]); - IF s != '' THEN - w := getorcreate_word_id(s); - END IF; - END LOOP; - END IF; - - s := regexp_replace(src, '市$', ''); - IF s != src THEN - s := make_standard_name(s); - IF s != '' THEN - w := getorcreate_name_id(s, src); - END IF; - END IF; - - RETURN 1; -END; -$$ -LANGUAGE plpgsql; diff --git a/lib-sql/tokenizer/legacy_tokenizer_indices.sql b/lib-sql/tokenizer/legacy_tokenizer_indices.sql deleted file mode 100644 index 016c518a..00000000 --- a/lib-sql/tokenizer/legacy_tokenizer_indices.sql +++ /dev/null @@ -1,10 +0,0 @@ --- SPDX-License-Identifier: GPL-2.0-only --- --- This file is part of Nominatim. (https://nominatim.org) --- --- Copyright (C) 2022 by the Nominatim developer community. --- For a full list of authors see the git log. - --- Required for details lookup. -CREATE INDEX IF NOT EXISTS idx_word_word_id - ON word USING BTREE (word_id) {{db.tablespace.search_index}}; diff --git a/lib-sql/tokenizer/legacy_tokenizer_tables.sql b/lib-sql/tokenizer/legacy_tokenizer_tables.sql deleted file mode 100644 index 7969f1ca..00000000 --- a/lib-sql/tokenizer/legacy_tokenizer_tables.sql +++ /dev/null @@ -1,28 +0,0 @@ --- SPDX-License-Identifier: GPL-2.0-only --- --- This file is part of Nominatim. (https://nominatim.org) --- --- Copyright (C) 2022 by the Nominatim developer community. --- For a full list of authors see the git log. - -DROP TABLE IF EXISTS word; -CREATE TABLE word ( - word_id INTEGER, - word_token text NOT NULL, - word text, - class text, - type text, - country_code varchar(2), - search_name_count INTEGER, - operator TEXT -) {{db.tablespace.search_data}}; - -CREATE INDEX idx_word_word_token ON word - USING BTREE (word_token) {{db.tablespace.search_index}}; -CREATE INDEX idx_word_word ON word - USING BTREE (word) {{db.tablespace.search_index}} WHERE word is not null; -GRANT SELECT ON word TO "{{config.DATABASE_WEBUSER}}"; - -DROP SEQUENCE IF EXISTS seq_word; -CREATE SEQUENCE seq_word start 1; -GRANT SELECT ON seq_word to "{{config.DATABASE_WEBUSER}}"; diff --git a/mkdocs.yml b/mkdocs.yml index 3c1ff80b..6a24e816 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,12 @@ site_name: Nominatim Manual theme: + font: false name: material features: - navigation.tabs + - toc.integrate + plugins: + - privacy copyright: Copyright © Nominatim developer community docs_dir: docs site_url: https://nominatim.org @@ -22,8 +26,7 @@ nav: - 'Basic Installation': 'admin/Installation.md' - 'Import' : 'admin/Import.md' - 'Update' : 'admin/Update.md' - - 'Deploy (Python frontend)' : 'admin/Deployment-Python.md' - - 'Deploy (PHP frontend)' : 'admin/Deployment-PHP.md' + - 'Deploy' : 'admin/Deployment-Python.md' - 'Nominatim UI' : 'admin/Setup-Nominatim-UI.md' - 'Advanced Installations' : 'admin/Advanced-Installations.md' - 'Maintenance' : 'admin/Maintenance.md' @@ -68,7 +71,8 @@ markdown_extensions: alternate_style: true - def_list - toc: - permalink:  + toc_depth: 4 + permalink: 🔗 extra_css: [extra.css, styles.css] exclude_docs: | mk_install_instructions.py diff --git a/module/CMakeLists.txt b/module/CMakeLists.txt deleted file mode 100644 index 01831f90..00000000 --- a/module/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -# just use the pgxs makefile - -foreach(suffix ${PostgreSQL_ADDITIONAL_VERSIONS} "16" "15" "14" "13" "12" "11" "10" "9.6") - list(APPEND PG_CONFIG_HINTS - "/usr/pgsql-${suffix}/bin") -endforeach() - -find_program(PG_CONFIG pg_config HINTS ${PG_CONFIG_HINTS}) - - - -execute_process(COMMAND ${PG_CONFIG} --pgxs - OUTPUT_VARIABLE PGXS - OUTPUT_STRIP_TRAILING_WHITESPACE) - -if (NOT EXISTS "${PGXS}") -message(FATAL_ERROR "Postgresql server package not found.") -endif() - -ADD_CUSTOM_COMMAND( OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/dummy - COMMAND PGXS=${PGXS} PG_CONFIG=${PG_CONFIG} MODSRCDIR=${CMAKE_CURRENT_SOURCE_DIR} $(MAKE) -f ${CMAKE_CURRENT_SOURCE_DIR}/Makefile - COMMENT "Running external makefile ${PGXS}" - ) - -ADD_CUSTOM_TARGET( nominatim_lib ALL - DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/dummy - ) - diff --git a/module/Makefile b/module/Makefile deleted file mode 100644 index dd037a01..00000000 --- a/module/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -MODULES = nominatim -PG_CPPFLAGS = -I$(MODSRCDIR) -include $(PGXS) - -VPATH = $(MODSRCDIR) - -all: - chmod 755 nominatim.so - -install: - @echo Library does not need to be installed. diff --git a/module/nominatim.c b/module/nominatim.c deleted file mode 100644 index 54632f76..00000000 --- a/module/nominatim.c +++ /dev/null @@ -1,301 +0,0 @@ -/** - * SPDX-License-Identifier: GPL-2.0-only - * - * This file is part of Nominatim. (https://nominatim.org) - * - * Copyright (C) 2022 by the Nominatim developer community. - * For a full list of authors see the git log. - */ -#include "postgres.h" -#include "fmgr.h" -#include "mb/pg_wchar.h" -#include - -#if PG_MAJORVERSION_NUM > 15 -#include "varatt.h" -#endif - -PG_MODULE_MAGIC; - -Datum transliteration( PG_FUNCTION_ARGS ); -Datum gettokenstring( PG_FUNCTION_ARGS ); -void str_replace(char* buffer, int* len, int* changes, char* from, int fromlen, char* to, int tolen, int); -void str_dupspaces(char* buffer); - -PG_FUNCTION_INFO_V1( transliteration ); -Datum -transliteration( PG_FUNCTION_ARGS ) -{ - static char * ascii = UTFASCII; - static uint16 asciilookup[65536] = UTFASCIILOOKUP; - char * asciipos; - - text *source; - unsigned char *sourcedata; - int sourcedatalength; - - unsigned int c1,c2,c3,c4; - unsigned int * wchardata; - unsigned int * wchardatastart; - - text *result; - unsigned char *resultdata; - int resultdatalength; - int iLen; - - if (GetDatabaseEncoding() != PG_UTF8) - { - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("requires UTF8 database encoding"))); - } - - if (PG_ARGISNULL(0)) - { - PG_RETURN_NULL(); - } - - // The original string - source = PG_GETARG_TEXT_P(0); - sourcedata = (unsigned char *)VARDATA(source); - sourcedatalength = VARSIZE(source) - VARHDRSZ; - - // Intermediate wchar version of string - wchardatastart = wchardata = (unsigned int *)palloc((sourcedatalength+1)*sizeof(int)); - - // Based on pg_utf2wchar_with_len from wchar.c - // Postgresql strings are not zero terminalted - while (sourcedatalength > 0) - { - if ((*sourcedata & 0x80) == 0) - { - *wchardata = *sourcedata++; - wchardata++; - sourcedatalength--; - } - else if ((*sourcedata & 0xe0) == 0xc0) - { - if (sourcedatalength < 2) break; - c1 = *sourcedata++ & 0x1f; - c2 = *sourcedata++ & 0x3f; - *wchardata = (c1 << 6) | c2; - if (*wchardata < 65536) wchardata++; - sourcedatalength -= 2; - } - else if ((*sourcedata & 0xf0) == 0xe0) - { - if (sourcedatalength < 3) break; - c1 = *sourcedata++ & 0x0f; - c2 = *sourcedata++ & 0x3f; - c3 = *sourcedata++ & 0x3f; - *wchardata = (c1 << 12) | (c2 << 6) | c3; - if (*wchardata < 65536) wchardata++; - sourcedatalength -= 3; - } - else if ((*sourcedata & 0xf8) == 0xf0) - { - if (sourcedatalength < 4) break; - c1 = *sourcedata++ & 0x07; - c2 = *sourcedata++ & 0x3f; - c3 = *sourcedata++ & 0x3f; - c4 = *sourcedata++ & 0x3f; - *wchardata = (c1 << 18) | (c2 << 12) | (c3 << 6) | c4; - if (*wchardata < 65536) wchardata++; - sourcedatalength -= 4; - } - else if ((*sourcedata & 0xfc) == 0xf8) - { - // table does not extend beyond 4 char long, just skip - if (sourcedatalength < 5) break; - sourcedatalength -= 5; - sourcedata += 5; - } - else if ((*sourcedata & 0xfe) == 0xfc) - { - // table does not extend beyond 4 char long, just skip - if (sourcedatalength < 6) break; - sourcedatalength -= 6; - sourcedata += 6; - } - else - { - // assume lenngth 1, silently drop bogus characters - sourcedatalength--; - sourcedata += 1; - } - } - *wchardata = 0; - - // calc the length of transliteration string - resultdatalength = 0; - wchardata = wchardatastart; - while(*wchardata) - { - if (*(asciilookup + *wchardata) > 0) resultdatalength += *(ascii + *(asciilookup + *wchardata)); - wchardata++; - } - - // allocate & create the result - result = (text *)palloc(resultdatalength + VARHDRSZ); - SET_VARSIZE(result, resultdatalength + VARHDRSZ); - resultdata = (unsigned char *)VARDATA(result); - - wchardata = wchardatastart; - while(*wchardata) - { - if (*(asciilookup + *wchardata) > 0) - { - asciipos = ascii + *(asciilookup + *wchardata); - for(iLen = *asciipos; iLen > 0; iLen--) - { - asciipos++; - *resultdata = *asciipos; - resultdata++; - } - } - /*else - { - ereport( WARNING, ( errcode( ERRCODE_SUCCESSFUL_COMPLETION ), - errmsg( "missing char: %i\n", *wchardata ))); - - }*/ - wchardata++; - } - - pfree(wchardatastart); - - PG_RETURN_TEXT_P(result); -} - -// Set isspace=1 if the replacement _only_ adds a space before the search string. I.e. to == " " + from -void str_replace(char* buffer, int* len, int* changes, char* from, int fromlen, char* to, int tolen, int isspace) -{ - char *p; - - // Search string is too long to be present - if (fromlen > *len) return; - - p = strstr(buffer, from); - while(p) - { - if (!isspace || (p > buffer && *(p-1) != ' ')) - { - (*changes)++; - if (tolen != fromlen) memmove(p+tolen, p+fromlen, *len-(p-buffer)+1); - memcpy(p, to, tolen); - *len += tolen - fromlen; - } - p = strstr(p+1, from); - } -} - -void str_dupspaces(char* buffer) -{ - char *out; - int wasspace; - - out = buffer; - wasspace = 0; - while(*buffer) - { - if (wasspace && *buffer != ' ') wasspace = 0; - if (!wasspace) - { - *out = *buffer; - out++; - wasspace = (*buffer == ' '); - } - buffer++; - } - *out = 0; -} - -PG_FUNCTION_INFO_V1( gettokenstring ); -Datum -gettokenstring( PG_FUNCTION_ARGS ) -{ - text *source; - unsigned char *sourcedata; - int sourcedatalength; - - char * buffer; - int len; - int changes; - - text *result; - - if (GetDatabaseEncoding() != PG_UTF8) - { - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("requires UTF8 database encoding"))); - } - - if (PG_ARGISNULL(0)) - { - PG_RETURN_NULL(); - } - - // The original string - source = PG_GETARG_TEXT_P(0); - sourcedata = (unsigned char *)VARDATA(source); - sourcedatalength = VARSIZE(source) - VARHDRSZ; - - // Buffer for doing the replace in - string could get slightly longer (double is massive overkill) - buffer = (char *)palloc((sourcedatalength*2)*sizeof(char)); - memcpy(buffer+1, sourcedata, sourcedatalength); - buffer[0] = 32; - buffer[sourcedatalength+1] = 32; - buffer[sourcedatalength+2] = 0; - len = sourcedatalength+3; - - changes = 1; - str_dupspaces(buffer); - while(changes) - { - changes = 0; - #include - str_dupspaces(buffer); - } - - // 'and' in various languages - str_replace(buffer, &len, &changes, " and ", 5, " ", 1, 0); - str_replace(buffer, &len, &changes, " und ", 5, " ", 1, 0); - str_replace(buffer, &len, &changes, " en ", 4, " ", 1, 0); - str_replace(buffer, &len, &changes, " et ", 4, " ", 1, 0); - str_replace(buffer, &len, &changes, " y ", 3, " ", 1, 0); - - // 'the' (and similar) - str_replace(buffer, &len, &changes, " the ", 5, " ", 1, 0); - str_replace(buffer, &len, &changes, " der ", 5, " ", 1, 0); - str_replace(buffer, &len, &changes, " den ", 5, " ", 1, 0); - str_replace(buffer, &len, &changes, " die ", 5, " ", 1, 0); - str_replace(buffer, &len, &changes, " das ", 5, " ", 1, 0); - str_replace(buffer, &len, &changes, " la ", 4, " ", 1, 0); - str_replace(buffer, &len, &changes, " le ", 4, " ", 1, 0); - str_replace(buffer, &len, &changes, " el ", 4, " ", 1, 0); - str_replace(buffer, &len, &changes, " il ", 4, " ", 1, 0); - - // german - str_replace(buffer, &len, &changes, "ae", 2, "a", 1, 0); - str_replace(buffer, &len, &changes, "oe", 2, "o", 1, 0); - str_replace(buffer, &len, &changes, "ue", 2, "u", 1, 0); - str_replace(buffer, &len, &changes, "sss", 3, "ss", 2, 0); - str_replace(buffer, &len, &changes, "ih", 2, "i", 1, 0); - str_replace(buffer, &len, &changes, "eh", 2, "e", 1, 0); - - // russian - str_replace(buffer, &len, &changes, "ie", 2, "i", 1, 0); - str_replace(buffer, &len, &changes, "yi", 2, "i", 1, 0); - - // allocate & create the result - len--;// Drop the terminating zero - result = (text *)palloc(len + VARHDRSZ); - SET_VARSIZE(result, len + VARHDRSZ); - memcpy(VARDATA(result), buffer, len); - - pfree(buffer); - - PG_RETURN_TEXT_P(result); -} - diff --git a/module/tokenstringreplacements.inc b/module/tokenstringreplacements.inc deleted file mode 100644 index 9f4975ab..00000000 --- a/module/tokenstringreplacements.inc +++ /dev/null @@ -1,884 +0,0 @@ -/** - * SPDX-License-Identifier: GPL-2.0-only - * - * This file is part of Nominatim. (https://nominatim.org) - * - * Copyright (C) 2022 by the Nominatim developer community. - * For a full list of authors see the git log. - */ - str_replace(buffer, &len, &changes, " national wildlife refuge area ", 31, " nwra ", 6, 0); - str_replace(buffer, &len, &changes, " national recreation area ", 26, " nra ", 5, 0); - str_replace(buffer, &len, &changes, " air national guard base ", 25, " angb ", 6, 0); - str_replace(buffer, &len, &changes, " zhilishchien komplieks ", 24, " zh k ", 6, 0); - str_replace(buffer, &len, &changes, " trung tam thuong mdhi ", 23, " tttm ", 6, 0); - str_replace(buffer, &len, &changes, " poligono industrial ", 21, " pgind ", 7, 0); - str_replace(buffer, &len, &changes, " trung hoc pho thong ", 21, " thpt ", 6, 0); - str_replace(buffer, &len, &changes, " onze lieve vrouw e ", 20, " olv ", 5, 0); - str_replace(buffer, &len, &changes, " strada provinciale ", 20, " sp ", 4, 0); - str_replace(buffer, &len, &changes, "onze lieve vrouw e ", 19, " olv ", 5, 0); - str_replace(buffer, &len, &changes, " punto kilometrico ", 19, " pk ", 4, 0); - str_replace(buffer, &len, &changes, " cong vien van hoa ", 19, " cvvh ", 6, 0); - str_replace(buffer, &len, &changes, " can cu khong quan ", 19, " cckq ", 6, 0); - str_replace(buffer, &len, &changes, "strada provinciale ", 19, " sp ", 4, 0); - str_replace(buffer, &len, &changes, " strada regionale ", 18, " sr ", 4, 0); - str_replace(buffer, &len, &changes, " strada comunale ", 17, " sc ", 4, 0); - str_replace(buffer, &len, &changes, "strada regionale ", 17, " sr ", 4, 0); - str_replace(buffer, &len, &changes, " trung hoc co so ", 17, " thcs ", 6, 0); - str_replace(buffer, &len, &changes, " san bay quoc te ", 17, " sbqt ", 6, 0); - str_replace(buffer, &len, &changes, " cong ty co phyn ", 17, " ctcp ", 6, 0); - str_replace(buffer, &len, &changes, " khu cong nghiep ", 17, " kcn ", 5, 0); - str_replace(buffer, &len, &changes, " air force base ", 16, " afb ", 5, 0); - str_replace(buffer, &len, &changes, " strada statale ", 16, " ss ", 4, 0); - str_replace(buffer, &len, &changes, " vien bcyo tang ", 16, " vbt ", 5, 0); - str_replace(buffer, &len, &changes, "strada comunale ", 16, " sc ", 4, 0); - str_replace(buffer, &len, &changes, " circunvalacion ", 16, " ccvcn ", 7, 0); - str_replace(buffer, &len, &changes, " paseo maritimo ", 16, " psmar ", 7, 0); - str_replace(buffer, &len, &changes, " wielkopolskie ", 15, " wlkp ", 6, 0); - str_replace(buffer, &len, &changes, " national park ", 15, " np ", 4, 0); - str_replace(buffer, &len, &changes, " middle school ", 15, " ms ", 4, 0); - str_replace(buffer, &len, &changes, " international ", 15, " intl ", 6, 0); - str_replace(buffer, &len, &changes, " burgermeister ", 15, " bgm ", 5, 0); - str_replace(buffer, &len, &changes, " vuon quoc gia ", 15, " vqg ", 5, 0); - str_replace(buffer, &len, &changes, " qucyng truong ", 15, " qt ", 4, 0); - str_replace(buffer, &len, &changes, "strada statale ", 15, " ss ", 4, 0); - str_replace(buffer, &len, &changes, " state highway ", 15, " sh ", 4, 0); - str_replace(buffer, &len, &changes, "burgermeister ", 14, " bgm ", 5, 0); - str_replace(buffer, &len, &changes, " right of way ", 14, " rowy ", 6, 0); - str_replace(buffer, &len, &changes, " hauptbahnhof ", 14, " hbf ", 5, 0); - str_replace(buffer, &len, &changes, " apartamentos ", 14, " aptos ", 7, 0); - str_replace(buffer, &len, &changes, " wielkopolski ", 14, " wlkp ", 6, 0); - str_replace(buffer, &len, &changes, " burgemeester ", 14, " bg ", 4, 0); - str_replace(buffer, &len, &changes, " camino nuevo ", 14, " c n ", 5, 0); - str_replace(buffer, &len, &changes, " camino hondo ", 14, " c h ", 5, 0); - str_replace(buffer, &len, &changes, " urbanizacion ", 14, " urb ", 5, 0); - str_replace(buffer, &len, &changes, " camino viejo ", 14, " c v ", 5, 0); - str_replace(buffer, &len, &changes, " wielkopolska ", 14, " wlkp ", 6, 0); - str_replace(buffer, &len, &changes, " wojewodztwie ", 14, " woj ", 5, 0); - str_replace(buffer, &len, &changes, " county route ", 14, " cr ", 4, 0); - str_replace(buffer, &len, &changes, " prolongacion ", 14, " prol ", 6, 0); - str_replace(buffer, &len, &changes, " thoroughfare ", 14, " thor ", 6, 0); - str_replace(buffer, &len, &changes, " san van dong ", 14, " svd ", 5, 0); - str_replace(buffer, &len, &changes, " tong cong ty ", 14, " tct ", 5, 0); - str_replace(buffer, &len, &changes, " khu nghi mat ", 14, " knm ", 5, 0); - str_replace(buffer, &len, &changes, " nha thi dzu ", 13, " ntd ", 5, 0); - str_replace(buffer, &len, &changes, " khu du lich ", 13, " kdl ", 5, 0); - str_replace(buffer, &len, &changes, " demarcacion ", 13, " demar ", 7, 0); - str_replace(buffer, &len, &changes, " cau ldhc bo ", 13, " clb ", 5, 0); - str_replace(buffer, &len, &changes, " interchange ", 13, " intg ", 6, 0); - str_replace(buffer, &len, &changes, " distributor ", 13, " dstr ", 6, 0); - str_replace(buffer, &len, &changes, " state route ", 13, " sr ", 4, 0); - str_replace(buffer, &len, &changes, " wojewodztwo ", 13, " woj ", 5, 0); - str_replace(buffer, &len, &changes, " reservation ", 13, " res ", 5, 0); - str_replace(buffer, &len, &changes, " monseigneur ", 13, " mgr ", 5, 0); - str_replace(buffer, &len, &changes, " transversal ", 13, " trval ", 7, 0); - str_replace(buffer, &len, &changes, " extrarradio ", 13, " extrr ", 7, 0); - str_replace(buffer, &len, &changes, " high school ", 13, " hs ", 4, 0); - str_replace(buffer, &len, &changes, " mazowieckie ", 13, " maz ", 5, 0); - str_replace(buffer, &len, &changes, " residencial ", 13, " resid ", 7, 0); - str_replace(buffer, &len, &changes, " cong truong ", 13, " ct ", 4, 0); - str_replace(buffer, &len, &changes, " cooperativa ", 13, " coop ", 6, 0); - str_replace(buffer, &len, &changes, " diseminado ", 12, " disem ", 7, 0); - str_replace(buffer, &len, &changes, " barranquil ", 12, " bqllo ", 7, 0); - str_replace(buffer, &len, &changes, " fire track ", 12, " ftrk ", 6, 0); - str_replace(buffer, &len, &changes, " south east ", 12, " se ", 4, 0); - str_replace(buffer, &len, &changes, " north east ", 12, " ne ", 4, 0); - str_replace(buffer, &len, &changes, " university ", 12, " univ ", 6, 0); - str_replace(buffer, &len, &changes, " south west ", 12, " sw ", 4, 0); - str_replace(buffer, &len, &changes, " monasterio ", 12, " mtrio ", 7, 0); - str_replace(buffer, &len, &changes, " vecindario ", 12, " vecin ", 7, 0); - str_replace(buffer, &len, &changes, " carreterin ", 12, " ctrin ", 7, 0); - str_replace(buffer, &len, &changes, " callejuela ", 12, " cjla ", 6, 0); - str_replace(buffer, &len, &changes, " north-east ", 12, " ne ", 4, 0); - str_replace(buffer, &len, &changes, " south-west ", 12, " sw ", 4, 0); - str_replace(buffer, &len, &changes, " gebroeders ", 12, " gebr ", 6, 0); - str_replace(buffer, &len, &changes, " serviceway ", 12, " swy ", 5, 0); - str_replace(buffer, &len, &changes, " quadrangle ", 12, " qdgl ", 6, 0); - str_replace(buffer, &len, &changes, " commandant ", 12, " cmdt ", 6, 0); - str_replace(buffer, &len, &changes, " extramuros ", 12, " extrm ", 7, 0); - str_replace(buffer, &len, &changes, " escalinata ", 12, " escal ", 7, 0); - str_replace(buffer, &len, &changes, " north-west ", 12, " n ", 3, 0); - str_replace(buffer, &len, &changes, " bulevardul ", 12, " bd ", 4, 0); - str_replace(buffer, &len, &changes, " particular ", 12, " parti ", 7, 0); - str_replace(buffer, &len, &changes, " mazowiecka ", 12, " maz ", 5, 0); - str_replace(buffer, &len, &changes, " mazowiecki ", 12, " maz ", 5, 0); - str_replace(buffer, &len, &changes, " north west ", 12, " n ", 3, 0); - str_replace(buffer, &len, &changes, " industrial ", 12, " ind ", 5, 0); - str_replace(buffer, &len, &changes, " costanilla ", 12, " cstan ", 7, 0); - str_replace(buffer, &len, &changes, " khach sdhn ", 12, " ks ", 4, 0); - str_replace(buffer, &len, &changes, " south-east ", 12, " se ", 4, 0); - str_replace(buffer, &len, &changes, " phi truong ", 12, " pt ", 4, 0); - str_replace(buffer, &len, &changes, " expressway ", 12, " exp ", 5, 0); - str_replace(buffer, &len, &changes, " fondamenta ", 12, " f ta ", 6, 0); - str_replace(buffer, &len, &changes, " apartments ", 12, " apts ", 6, 0); - str_replace(buffer, &len, &changes, " cul de sac ", 12, " cds ", 5, 0); - str_replace(buffer, &len, &changes, " corralillo ", 12, " crrlo ", 7, 0); - str_replace(buffer, &len, &changes, " mitropolit ", 12, " mit ", 5, 0); - str_replace(buffer, &len, &changes, " etorbidea ", 11, " etorb ", 7, 0); - str_replace(buffer, &len, &changes, " ploshchad ", 11, " pl ", 4, 0); - str_replace(buffer, &len, &changes, " cobertizo ", 11, " cbtiz ", 7, 0); - str_replace(buffer, &len, &changes, " underpass ", 11, " upas ", 6, 0); - str_replace(buffer, &len, &changes, " crossroad ", 11, " crd ", 5, 0); - str_replace(buffer, &len, &changes, " fundatura ", 11, " fnd ", 5, 0); - str_replace(buffer, &len, &changes, " foreshore ", 11, " fshr ", 6, 0); - str_replace(buffer, &len, &changes, " parklands ", 11, " pkld ", 6, 0); - str_replace(buffer, &len, &changes, " esplanade ", 11, " esp ", 5, 0); - str_replace(buffer, &len, &changes, " centreway ", 11, " cnwy ", 6, 0); - str_replace(buffer, &len, &changes, " formation ", 11, " form ", 6, 0); - str_replace(buffer, &len, &changes, " explanada ", 11, " expla ", 7, 0); - str_replace(buffer, &len, &changes, " viviendas ", 11, " vvdas ", 7, 0); - str_replace(buffer, &len, &changes, " northeast ", 11, " ne ", 4, 0); - str_replace(buffer, &len, &changes, " cong vien ", 11, " cv ", 4, 0); - str_replace(buffer, &len, &changes, " northwest ", 11, " n ", 3, 0); - str_replace(buffer, &len, &changes, " buildings ", 11, " bldgs ", 7, 0); - str_replace(buffer, &len, &changes, " errepidea ", 11, " err ", 5, 0); - str_replace(buffer, &len, &changes, " extension ", 11, " ex ", 4, 0); - str_replace(buffer, &len, &changes, " municipal ", 11, " mun ", 5, 0); - str_replace(buffer, &len, &changes, " southeast ", 11, " se ", 4, 0); - str_replace(buffer, &len, &changes, " sanatorio ", 11, " sanat ", 7, 0); - str_replace(buffer, &len, &changes, " thanh pho ", 11, " tp ", 4, 0); - str_replace(buffer, &len, &changes, " firetrail ", 11, " fit ", 5, 0); - str_replace(buffer, &len, &changes, " santuario ", 11, " santu ", 7, 0); - str_replace(buffer, &len, &changes, " southwest ", 11, " sw ", 4, 0); - str_replace(buffer, &len, &changes, " autopista ", 11, " auto ", 6, 0); - str_replace(buffer, &len, &changes, " president ", 11, " pres ", 6, 0); - str_replace(buffer, &len, &changes, " rinconada ", 11, " rcda ", 6, 0); - str_replace(buffer, &len, &changes, " kardinaal ", 11, " kard ", 6, 0); - str_replace(buffer, &len, &changes, " plazoleta ", 11, " pzta ", 6, 0); - str_replace(buffer, &len, &changes, " duong sat ", 11, " ds ", 4, 0); - str_replace(buffer, &len, &changes, " trung tam ", 11, " tt ", 4, 0); - str_replace(buffer, &len, &changes, " piazzetta ", 11, " pta ", 5, 0); - str_replace(buffer, &len, &changes, " boardwalk ", 11, " bwlk ", 6, 0); - str_replace(buffer, &len, &changes, " bulievard ", 11, " bd ", 4, 0); - str_replace(buffer, &len, &changes, " luitenant ", 11, " luit ", 6, 0); - str_replace(buffer, &len, &changes, " courtyard ", 11, " ctyd ", 6, 0); - str_replace(buffer, &len, &changes, " reservoir ", 11, " res ", 5, 0); - str_replace(buffer, &len, &changes, " bulevardu ", 11, " bd ", 4, 0); - str_replace(buffer, &len, &changes, " community ", 11, " comm ", 6, 0); - str_replace(buffer, &len, &changes, " concourse ", 11, " con ", 5, 0); - str_replace(buffer, &len, &changes, " profiesor ", 11, " prof ", 6, 0); - str_replace(buffer, &len, &changes, " promenade ", 11, " prom ", 6, 0); - str_replace(buffer, &len, &changes, " gienieral ", 11, " ghien ", 7, 0); - str_replace(buffer, &len, &changes, " puistikko ", 11, " pko ", 5, 0); - str_replace(buffer, &len, &changes, " balneario ", 11, " balnr ", 7, 0); - str_replace(buffer, &len, &changes, " carretera ", 11, " ctra ", 6, 0); - str_replace(buffer, &len, &changes, " ingenieur ", 11, " ir ", 4, 0); - str_replace(buffer, &len, &changes, " boulevard ", 11, " bd ", 4, 0); - str_replace(buffer, &len, &changes, " deviation ", 11, " devn ", 6, 0); - str_replace(buffer, &len, &changes, " hipodromo ", 11, " hipod ", 7, 0); - str_replace(buffer, &len, &changes, " professor ", 11, " prof ", 6, 0); - str_replace(buffer, &len, &changes, " triangle ", 10, " tri ", 5, 0); - str_replace(buffer, &len, &changes, " dotsient ", 10, " dots ", 6, 0); - str_replace(buffer, &len, &changes, " boundary ", 10, " bdy ", 5, 0); - str_replace(buffer, &len, &changes, " salizada ", 10, " s da ", 6, 0); - str_replace(buffer, &len, &changes, " trunkway ", 10, " tkwy ", 6, 0); - str_replace(buffer, &len, &changes, " cinturon ", 10, " cint ", 6, 0); - str_replace(buffer, &len, &changes, "president ", 10, " pres ", 6, 0); - str_replace(buffer, &len, &changes, " military ", 10, " mil ", 5, 0); - str_replace(buffer, &len, &changes, " jonkheer ", 10, " jhr ", 5, 0); - str_replace(buffer, &len, &changes, " motorway ", 10, " mwy ", 5, 0); - str_replace(buffer, &len, &changes, " steenweg ", 10, " stwg ", 6, 0); - str_replace(buffer, &len, &changes, " crescent ", 10, " cr ", 4, 0); - str_replace(buffer, &len, &changes, " kanunnik ", 10, " kan ", 5, 0); - str_replace(buffer, &len, &changes, " koningin ", 10, " kon ", 5, 0); - str_replace(buffer, &len, &changes, " crossing ", 10, " xing ", 6, 0); - str_replace(buffer, &len, &changes, " callejon ", 10, " cjon ", 6, 0); - str_replace(buffer, &len, &changes, " pasadizo ", 10, " pzo ", 5, 0); - str_replace(buffer, &len, &changes, " crossway ", 10, " cowy ", 6, 0); - str_replace(buffer, &len, &changes, " cottages ", 10, " cotts ", 7, 0); - str_replace(buffer, &len, &changes, " mountain ", 10, " mtn ", 5, 0); - str_replace(buffer, &len, &changes, " business ", 10, " bus ", 5, 0); - str_replace(buffer, &len, &changes, " pierwszy ", 10, " 1 ", 3, 0); - str_replace(buffer, &len, &changes, " pierwsza ", 10, " 1 ", 3, 0); - str_replace(buffer, &len, &changes, " pierwsze ", 10, " 1 ", 3, 0); - str_replace(buffer, &len, &changes, " barriada ", 10, " barda ", 7, 0); - str_replace(buffer, &len, &changes, " entrance ", 10, " ent ", 5, 0); - str_replace(buffer, &len, &changes, " causeway ", 10, " cway ", 6, 0); - str_replace(buffer, &len, &changes, " generaal ", 10, " gen ", 5, 0); - str_replace(buffer, &len, &changes, " driveway ", 10, " dvwy ", 6, 0); - str_replace(buffer, &len, &changes, " township ", 10, " twp ", 5, 0); - str_replace(buffer, &len, &changes, " stazione ", 10, " staz ", 6, 0); - str_replace(buffer, &len, &changes, " broadway ", 10, " bway ", 6, 0); - str_replace(buffer, &len, &changes, " alleyway ", 10, " alwy ", 6, 0); - str_replace(buffer, &len, &changes, " quadrant ", 10, " qdrt ", 6, 0); - str_replace(buffer, &len, &changes, " apeadero ", 10, " apdro ", 7, 0); - str_replace(buffer, &len, &changes, " arboleda ", 10, " arb ", 5, 0); - str_replace(buffer, &len, &changes, " escalera ", 10, " esca ", 6, 0); - str_replace(buffer, &len, &changes, " rdhp hat ", 10, " rh ", 4, 0); - str_replace(buffer, &len, &changes, " transito ", 10, " trans ", 7, 0); - str_replace(buffer, &len, &changes, " ddhi hoc ", 10, " dh ", 4, 0); - str_replace(buffer, &len, &changes, " travesia ", 10, " trva ", 6, 0); - str_replace(buffer, &len, &changes, " barranco ", 10, " branc ", 7, 0); - str_replace(buffer, &len, &changes, " namestie ", 10, " nam ", 5, 0); - str_replace(buffer, &len, &changes, " viaducto ", 10, " vcto ", 6, 0); - str_replace(buffer, &len, &changes, " convento ", 10, " cnvto ", 7, 0); - str_replace(buffer, &len, &changes, " estacion ", 10, " estcn ", 7, 0); - str_replace(buffer, &len, &changes, "puistikko ", 10, " pko ", 5, 0); - str_replace(buffer, &len, &changes, " precinct ", 10, " pct ", 5, 0); - str_replace(buffer, &len, &changes, " heiligen ", 10, " hl ", 4, 0); - str_replace(buffer, &len, &changes, " edificio ", 10, " edifc ", 7, 0); - str_replace(buffer, &len, &changes, " prazuela ", 10, " przla ", 7, 0); - str_replace(buffer, &len, &changes, " thi trzn ", 10, " tt ", 4, 0); - str_replace(buffer, &len, &changes, " ridgeway ", 10, " rgwy ", 6, 0); - str_replace(buffer, &len, &changes, " riverway ", 10, " rvwy ", 6, 0); - str_replace(buffer, &len, &changes, " corredor ", 10, " crrdo ", 7, 0); - str_replace(buffer, &len, &changes, " passatge ", 10, " ptge ", 6, 0); - str_replace(buffer, &len, &changes, " junction ", 10, " jnc ", 5, 0); - str_replace(buffer, &len, &changes, " hospital ", 10, " hosp ", 6, 0); - str_replace(buffer, &len, &changes, " highroad ", 10, " hrd ", 5, 0); - str_replace(buffer, &len, &changes, " torrente ", 10, " trrnt ", 7, 0); - str_replace(buffer, &len, &changes, " avinguda ", 10, " av ", 4, 0); - str_replace(buffer, &len, &changes, " portillo ", 10, " ptilo ", 7, 0); - str_replace(buffer, &len, &changes, " diagonal ", 10, " diag ", 6, 0); - str_replace(buffer, &len, &changes, " buu dien ", 10, " bd ", 4, 0); - str_replace(buffer, &len, &changes, " alqueria ", 10, " alque ", 7, 0); - str_replace(buffer, &len, &changes, " poligono ", 10, " polig ", 7, 0); - str_replace(buffer, &len, &changes, " roadside ", 10, " rdsd ", 6, 0); - str_replace(buffer, &len, &changes, " glorieta ", 10, " gta ", 5, 0); - str_replace(buffer, &len, &changes, " fundacul ", 10, " fdc ", 5, 0); - str_replace(buffer, &len, &changes, " cao dang ", 10, " cd ", 4, 0); - str_replace(buffer, &len, &changes, " rosebowl ", 10, " rsbl ", 6, 0); - str_replace(buffer, &len, &changes, " complejo ", 10, " compj ", 7, 0); - str_replace(buffer, &len, &changes, " carretil ", 10, " crtil ", 7, 0); - str_replace(buffer, &len, &changes, " intrarea ", 10, " int ", 5, 0); - str_replace(buffer, &len, &changes, " gran via ", 10, " g v ", 5, 0); - str_replace(buffer, &len, &changes, " approach ", 10, " app ", 5, 0); - str_replace(buffer, &len, &changes, " stradela ", 10, " sdla ", 6, 0); - str_replace(buffer, &len, &changes, " conjunto ", 10, " cjto ", 6, 0); - str_replace(buffer, &len, &changes, " arterial ", 10, " artl ", 6, 0); - str_replace(buffer, &len, &changes, " plazuela ", 10, " plzla ", 7, 0); - str_replace(buffer, &len, &changes, " frontage ", 10, " frtg ", 6, 0); - str_replace(buffer, &len, &changes, " faubourg ", 10, " fg ", 4, 0); - str_replace(buffer, &len, &changes, " mansions ", 10, " mans ", 6, 0); - str_replace(buffer, &len, &changes, " turnpike ", 10, " tpk ", 5, 0); - str_replace(buffer, &len, &changes, " piazzale ", 10, " p le ", 6, 0); - str_replace(buffer, &len, &changes, " tieu hoc ", 10, " th ", 4, 0); - str_replace(buffer, &len, &changes, " bulevard ", 10, " bd ", 4, 0); - str_replace(buffer, &len, &changes, " sendera ", 9, " sedra ", 7, 0); - str_replace(buffer, &len, &changes, " cutting ", 9, " cutt ", 6, 0); - str_replace(buffer, &len, &changes, " cantina ", 9, " canti ", 7, 0); - str_replace(buffer, &len, &changes, " cantera ", 9, " cantr ", 7, 0); - str_replace(buffer, &len, &changes, " rotonda ", 9, " rtda ", 6, 0); - str_replace(buffer, &len, &changes, " pasillo ", 9, " psllo ", 7, 0); - str_replace(buffer, &len, &changes, " landing ", 9, " ldg ", 5, 0); - str_replace(buffer, &len, &changes, " kolonel ", 9, " kol ", 5, 0); - str_replace(buffer, &len, &changes, " cong ty ", 9, " cty ", 5, 0); - str_replace(buffer, &len, &changes, " fairway ", 9, " fawy ", 6, 0); - str_replace(buffer, &len, &changes, " highway ", 9, " hwy ", 5, 0); - str_replace(buffer, &len, &changes, " lookout ", 9, " lkt ", 5, 0); - str_replace(buffer, &len, &changes, " meander ", 9, " mr ", 4, 0); - str_replace(buffer, &len, &changes, " carrera ", 9, " cra ", 5, 0); - str_replace(buffer, &len, &changes, " station ", 9, " stn ", 5, 0); - str_replace(buffer, &len, &changes, " kapitan ", 9, " kap ", 5, 0); - str_replace(buffer, &len, &changes, " medical ", 9, " med ", 5, 0); - str_replace(buffer, &len, &changes, " broeder ", 9, " br ", 4, 0); - str_replace(buffer, &len, &changes, " poblado ", 9, " pbdo ", 6, 0); - str_replace(buffer, &len, &changes, " impasse ", 9, " imp ", 5, 0); - str_replace(buffer, &len, &changes, " gardens ", 9, " gdn ", 5, 0); - str_replace(buffer, &len, &changes, " nha tho ", 9, " nt ", 4, 0); - str_replace(buffer, &len, &changes, " nha hat ", 9, " nh ", 4, 0); - str_replace(buffer, &len, &changes, " freeway ", 9, " fwy ", 5, 0); - str_replace(buffer, &len, &changes, " trasera ", 9, " tras ", 6, 0); - str_replace(buffer, &len, &changes, " portico ", 9, " prtco ", 7, 0); - str_replace(buffer, &len, &changes, " terrace ", 9, " ter ", 5, 0); - str_replace(buffer, &len, &changes, " heights ", 9, " hts ", 5, 0); - str_replace(buffer, &len, &changes, " camping ", 9, " campg ", 7, 0); - str_replace(buffer, &len, &changes, " callizo ", 9, " cllzo ", 7, 0); - str_replace(buffer, &len, &changes, " footway ", 9, " ftwy ", 6, 0); - str_replace(buffer, &len, &changes, " calzada ", 9, " czada ", 7, 0); - str_replace(buffer, &len, &changes, " dominee ", 9, " ds ", 4, 0); - str_replace(buffer, &len, &changes, " meadows ", 9, " mdws ", 6, 0); - str_replace(buffer, &len, &changes, " sendero ", 9, " send ", 6, 0); - str_replace(buffer, &len, &changes, " osiedle ", 9, " os ", 4, 0); - str_replace(buffer, &len, &changes, " estrada ", 9, " estda ", 7, 0); - str_replace(buffer, &len, &changes, " avenida ", 9, " av ", 4, 0); - str_replace(buffer, &len, &changes, " zgornji ", 9, " zg ", 4, 0); - str_replace(buffer, &len, &changes, " zgornje ", 9, " zg ", 4, 0); - str_replace(buffer, &len, &changes, " zgornja ", 9, " zg ", 4, 0); - str_replace(buffer, &len, &changes, " arrabal ", 9, " arral ", 7, 0); - str_replace(buffer, &len, &changes, " espalda ", 9, " eslda ", 7, 0); - str_replace(buffer, &len, &changes, " entrada ", 9, " entd ", 6, 0); - str_replace(buffer, &len, &changes, " kleiner ", 9, " kl ", 4, 0); - str_replace(buffer, &len, &changes, " kleines ", 9, " kl ", 4, 0); - str_replace(buffer, &len, &changes, " viaduct ", 9, " via ", 5, 0); - str_replace(buffer, &len, &changes, " roadway ", 9, " rdwy ", 6, 0); - str_replace(buffer, &len, &changes, " strasse ", 9, " st ", 4, 0); - str_replace(buffer, &len, &changes, " spodnje ", 9, " sp ", 4, 0); - str_replace(buffer, &len, &changes, " spodnji ", 9, " sp ", 4, 0); - str_replace(buffer, &len, &changes, " spodnja ", 9, " sp ", 4, 0); - str_replace(buffer, &len, &changes, " fabrica ", 9, " fca ", 5, 0); - str_replace(buffer, &len, &changes, " muntele ", 9, " mt ", 4, 0); - str_replace(buffer, &len, &changes, " maantee ", 9, " mt ", 4, 0); - str_replace(buffer, &len, &changes, " srednje ", 9, " sr ", 4, 0); - str_replace(buffer, &len, &changes, " unterer ", 9, " u ", 3, 0); - str_replace(buffer, &len, &changes, " unteres ", 9, " u ", 3, 0); - str_replace(buffer, &len, &changes, " plateau ", 9, " plat ", 6, 0); - str_replace(buffer, &len, &changes, " srednji ", 9, " sr ", 4, 0); - str_replace(buffer, &len, &changes, " empresa ", 9, " empr ", 6, 0); - str_replace(buffer, &len, &changes, " angosta ", 9, " angta ", 7, 0); - str_replace(buffer, &len, &changes, " costera ", 9, " coste ", 7, 0); - str_replace(buffer, &len, &changes, " tinh lo ", 9, " tl ", 4, 0); - str_replace(buffer, &len, &changes, " quoc lo ", 9, " ql ", 4, 0); - str_replace(buffer, &len, &changes, " auf der ", 9, " a d ", 5, 0); - str_replace(buffer, &len, &changes, " bulvari ", 9, " bl ", 4, 0); - str_replace(buffer, &len, &changes, " ddhi lo ", 9, " dl ", 4, 0); - str_replace(buffer, &len, &changes, " namesti ", 9, " nam ", 5, 0); - str_replace(buffer, &len, &changes, " passeig ", 9, " pg ", 4, 0); - str_replace(buffer, &len, &changes, " carrero ", 9, " cro ", 5, 0); - str_replace(buffer, &len, &changes, " cortijo ", 9, " crtjo ", 7, 0); - str_replace(buffer, &len, &changes, " san bay ", 9, " sb ", 4, 0); - str_replace(buffer, &len, &changes, " riviera ", 9, " rvra ", 6, 0); - str_replace(buffer, &len, &changes, " caddesi ", 9, " cd ", 4, 0); - str_replace(buffer, &len, &changes, " andador ", 9, " andad ", 7, 0); - str_replace(buffer, &len, &changes, " walkway ", 9, " wkwy ", 6, 0); - str_replace(buffer, &len, &changes, " granden ", 9, " gr ", 4, 0); - str_replace(buffer, &len, &changes, " grosser ", 9, " gr ", 4, 0); - str_replace(buffer, &len, &changes, " grosses ", 9, " gr ", 4, 0); - str_replace(buffer, &len, &changes, " reserve ", 9, " res ", 5, 0); - str_replace(buffer, &len, &changes, " alameda ", 9, " alam ", 6, 0); - str_replace(buffer, &len, &changes, " retreat ", 9, " rtt ", 5, 0); - str_replace(buffer, &len, &changes, " acequia ", 9, " aceq ", 6, 0); - str_replace(buffer, &len, &changes, " platsen ", 9, " pl ", 4, 0); - str_replace(buffer, &len, &changes, " bahnhof ", 9, " bf ", 4, 0); - str_replace(buffer, &len, &changes, " autovia ", 9, " autov ", 7, 0); - str_replace(buffer, &len, &changes, " srednja ", 9, " sr ", 4, 0); - str_replace(buffer, &len, &changes, " galeria ", 9, " gale ", 6, 0); - str_replace(buffer, &len, &changes, " circuit ", 9, " cct ", 5, 0); - str_replace(buffer, &len, &changes, " svingen ", 9, " sv ", 4, 0); - str_replace(buffer, &len, &changes, " plassen ", 9, " pl ", 4, 0); - str_replace(buffer, &len, &changes, " mirador ", 9, " mrdor ", 7, 0); - str_replace(buffer, &len, &changes, " laneway ", 9, " lnwy ", 6, 0); - str_replace(buffer, &len, &changes, " kolonia ", 9, " kol ", 5, 0); - str_replace(buffer, &len, &changes, " outlook ", 9, " otlk ", 6, 0); - str_replace(buffer, &len, &changes, " caravan ", 9, " cvn ", 5, 0); - str_replace(buffer, &len, &changes, " osiedlu ", 9, " os ", 4, 0); - str_replace(buffer, &len, &changes, " palacio ", 9, " palac ", 7, 0); - str_replace(buffer, &len, &changes, " pantano ", 9, " pant ", 6, 0); - str_replace(buffer, &len, &changes, " partida ", 9, " ptda ", 6, 0); - str_replace(buffer, &len, &changes, " calleja ", 9, " cllja ", 7, 0); - str_replace(buffer, &len, &changes, " mevrouw ", 9, " mevr ", 6, 0); - str_replace(buffer, &len, &changes, " meester ", 9, " mr ", 4, 0); - str_replace(buffer, &len, &changes, " pastoor ", 9, " past ", 6, 0); - str_replace(buffer, &len, &changes, " prinses ", 9, " pr ", 4, 0); - str_replace(buffer, &len, &changes, " bulevar ", 9, " bd ", 4, 0); - str_replace(buffer, &len, &changes, " tollway ", 9, " tlwy ", 6, 0); - str_replace(buffer, &len, &changes, "steenweg ", 9, " stwg ", 6, 0); - str_replace(buffer, &len, &changes, " caserio ", 9, " csrio ", 7, 0); - str_replace(buffer, &len, &changes, " mercado ", 9, " merc ", 6, 0); - str_replace(buffer, &len, &changes, " alejach ", 9, " al ", 4, 0); - str_replace(buffer, &len, &changes, " kvartal ", 9, " kv ", 4, 0); - str_replace(buffer, &len, &changes, " parkway ", 9, " pwy ", 5, 0); - str_replace(buffer, &len, &changes, " passage ", 9, " ps ", 4, 0); - str_replace(buffer, &len, &changes, " pathway ", 9, " pway ", 6, 0); - str_replace(buffer, &len, &changes, " splaiul ", 9, " sp ", 4, 0); - str_replace(buffer, &len, &changes, " soseaua ", 9, " sos ", 5, 0); - str_replace(buffer, &len, &changes, " colonia ", 9, " col ", 5, 0); - str_replace(buffer, &len, &changes, " wielkie ", 9, " wlk ", 5, 0); - str_replace(buffer, &len, &changes, " trzecie ", 9, " 3 ", 3, 0); - str_replace(buffer, &len, &changes, " llanura ", 9, " llnra ", 7, 0); - str_replace(buffer, &len, &changes, " malecon ", 9, " malec ", 7, 0); - str_replace(buffer, &len, &changes, " trzecia ", 9, " 3 ", 3, 0); - str_replace(buffer, &len, &changes, " trailer ", 9, " trlr ", 6, 0); - str_replace(buffer, &len, &changes, " cuadra ", 8, " cuadr ", 7, 0); - str_replace(buffer, &len, &changes, " cty cp ", 8, " ctcp ", 6, 0); - str_replace(buffer, &len, &changes, " paraje ", 8, " praje ", 7, 0); - str_replace(buffer, &len, &changes, " parque ", 8, " pque ", 6, 0); - str_replace(buffer, &len, &changes, " piazza ", 8, " p za ", 6, 0); - str_replace(buffer, &len, &changes, " puerta ", 8, " pta ", 5, 0); - str_replace(buffer, &len, &changes, " little ", 8, " lt ", 4, 0); - str_replace(buffer, &len, &changes, " pueblo ", 8, " pblo ", 6, 0); - str_replace(buffer, &len, &changes, " puente ", 8, " pnte ", 6, 0); - str_replace(buffer, &len, &changes, " jardin ", 8, " jdin ", 6, 0); - str_replace(buffer, &len, &changes, " granja ", 8, " granj ", 7, 0); - str_replace(buffer, &len, &changes, " market ", 8, " mkt ", 5, 0); - str_replace(buffer, &len, &changes, " pasaje ", 8, " psaje ", 7, 0); - str_replace(buffer, &len, &changes, " rotary ", 8, " rty ", 5, 0); - str_replace(buffer, &len, &changes, " corral ", 8, " crral ", 7, 0); - str_replace(buffer, &len, &changes, " siding ", 8, " sdng ", 6, 0); - str_replace(buffer, &len, &changes, " nucleo ", 8, " ncleo ", 7, 0); - str_replace(buffer, &len, &changes, " muelle ", 8, " muell ", 7, 0); - str_replace(buffer, &len, &changes, " carril ", 8, " crril ", 7, 0); - str_replace(buffer, &len, &changes, " portal ", 8, " prtal ", 7, 0); - str_replace(buffer, &len, &changes, " ramble ", 8, " rmbl ", 6, 0); - str_replace(buffer, &len, &changes, " pocket ", 8, " pkt ", 5, 0); - str_replace(buffer, &len, &changes, " chalet ", 8, " chlet ", 7, 0); - str_replace(buffer, &len, &changes, " canton ", 8, " cant ", 6, 0); - str_replace(buffer, &len, &changes, " ladera ", 8, " ldera ", 7, 0); - str_replace(buffer, &len, &changes, " parade ", 8, " pde ", 5, 0); - str_replace(buffer, &len, &changes, " dehesa ", 8, " dhsa ", 6, 0); - str_replace(buffer, &len, &changes, " museum ", 8, " mus ", 5, 0); - str_replace(buffer, &len, &changes, " middle ", 8, " mid ", 5, 0); - str_replace(buffer, &len, &changes, " cuesta ", 8, " custa ", 7, 0); - str_replace(buffer, &len, &changes, " gracht ", 8, " gr ", 4, 0); - str_replace(buffer, &len, &changes, " virful ", 8, " vf ", 4, 0); - str_replace(buffer, &len, &changes, " m tele ", 8, " mt ", 4, 0); - str_replace(buffer, &len, &changes, " varful ", 8, " vf ", 4, 0); - str_replace(buffer, &len, &changes, " str la ", 8, " sdla ", 6, 0); - str_replace(buffer, &len, &changes, " arcade ", 8, " arc ", 5, 0); - str_replace(buffer, &len, &changes, " strada ", 8, " st ", 4, 0); - str_replace(buffer, &len, &changes, " access ", 8, " accs ", 6, 0); - str_replace(buffer, &len, &changes, " bajada ", 8, " bjada ", 7, 0); - str_replace(buffer, &len, &changes, " veliki ", 8, " v ", 3, 0); - str_replace(buffer, &len, &changes, "strasse ", 8, " st ", 4, 0); - str_replace(buffer, &len, &changes, " velike ", 8, " v ", 3, 0); - str_replace(buffer, &len, &changes, " untere ", 8, " u ", 3, 0); - str_replace(buffer, &len, &changes, " velika ", 8, " v ", 3, 0); - str_replace(buffer, &len, &changes, " artery ", 8, " arty ", 6, 0); - str_replace(buffer, &len, &changes, " avenue ", 8, " av ", 4, 0); - str_replace(buffer, &len, &changes, " miasto ", 8, " m ", 3, 0); - str_replace(buffer, &len, &changes, " bypass ", 8, " byp ", 5, 0); - str_replace(buffer, &len, &changes, " placem ", 8, " pl ", 4, 0); - str_replace(buffer, &len, &changes, " barrio ", 8, " bo ", 4, 0); - str_replace(buffer, &len, &changes, " center ", 8, " ctr ", 5, 0); - str_replace(buffer, &len, &changes, " bldngs ", 8, " bldgs ", 7, 0); - str_replace(buffer, &len, &changes, " puerto ", 8, " pto ", 5, 0); - str_replace(buffer, &len, &changes, " wielka ", 8, " wlk ", 5, 0); - str_replace(buffer, &len, &changes, " tunnel ", 8, " tun ", 5, 0); - str_replace(buffer, &len, &changes, " wielki ", 8, " wlk ", 5, 0); - str_replace(buffer, &len, &changes, " bridge ", 8, " bri ", 5, 0); - str_replace(buffer, &len, &changes, " trzeci ", 8, " 3 ", 3, 0); - str_replace(buffer, &len, &changes, " veliko ", 8, " v ", 3, 0); - str_replace(buffer, &len, &changes, " quelle ", 8, " qu ", 4, 0); - str_replace(buffer, &len, &changes, " acceso ", 8, " acces ", 7, 0); - str_replace(buffer, &len, &changes, " bulvar ", 8, " bl ", 4, 0); - str_replace(buffer, &len, &changes, " sokagi ", 8, " sk ", 4, 0); - str_replace(buffer, &len, &changes, "platsen ", 8, " pl ", 4, 0); - str_replace(buffer, &len, &changes, " stigen ", 8, " st ", 4, 0); - str_replace(buffer, &len, &changes, " brucke ", 8, " br ", 4, 0); - str_replace(buffer, &len, &changes, " an der ", 8, " a d ", 5, 0); - str_replace(buffer, &len, &changes, " thi xa ", 8, " tx ", 4, 0); - str_replace(buffer, &len, &changes, " nordre ", 8, " ndr ", 5, 0); - str_replace(buffer, &len, &changes, " rambla ", 8, " rbla ", 6, 0); - str_replace(buffer, &len, &changes, " sondre ", 8, " sdr ", 5, 0); - str_replace(buffer, &len, &changes, "quoc lo ", 8, " ql ", 4, 0); - str_replace(buffer, &len, &changes, " phuong ", 8, " p ", 3, 0); - str_replace(buffer, &len, &changes, " vastra ", 8, " v ", 3, 0); - str_replace(buffer, &len, &changes, " carrer ", 8, " c ", 3, 0); - str_replace(buffer, &len, &changes, " oberes ", 8, " o ", 3, 0); - str_replace(buffer, &len, &changes, " raitti ", 8, " r ", 3, 0); - str_replace(buffer, &len, &changes, " puisto ", 8, " ps ", 4, 0); - str_replace(buffer, &len, &changes, " arroyo ", 8, " arry ", 6, 0); - str_replace(buffer, &len, &changes, " penger ", 8, " pgr ", 5, 0); - str_replace(buffer, &len, &changes, " oberer ", 8, " o ", 3, 0); - str_replace(buffer, &len, &changes, " kleine ", 8, " kl ", 4, 0); - str_replace(buffer, &len, &changes, " grosse ", 8, " gr ", 4, 0); - str_replace(buffer, &len, &changes, "granden ", 8, " gr ", 4, 0); - str_replace(buffer, &len, &changes, " villas ", 8, " vlls ", 6, 0); - str_replace(buffer, &len, &changes, " taival ", 8, " tvl ", 5, 0); - str_replace(buffer, &len, &changes, " in der ", 8, " i d ", 5, 0); - str_replace(buffer, &len, &changes, " centre ", 8, " ctr ", 5, 0); - str_replace(buffer, &len, &changes, " drugie ", 8, " 2 ", 3, 0); - str_replace(buffer, &len, &changes, " dokter ", 8, " dr ", 4, 0); - str_replace(buffer, &len, &changes, " grange ", 8, " gra ", 5, 0); - str_replace(buffer, &len, &changes, " doctor ", 8, " dr ", 4, 0); - str_replace(buffer, &len, &changes, " vicolo ", 8, " v lo ", 6, 0); - str_replace(buffer, &len, &changes, " kort e ", 8, " k ", 3, 0); - str_replace(buffer, &len, &changes, " koning ", 8, " kon ", 5, 0); - str_replace(buffer, &len, &changes, " straat ", 8, " st ", 4, 0); - str_replace(buffer, &len, &changes, " svieti ", 8, " sv ", 4, 0); - str_replace(buffer, &len, &changes, " callej ", 8, " cjon ", 6, 0); - str_replace(buffer, &len, &changes, " ground ", 8, " grnd ", 6, 0); - str_replace(buffer, &len, &changes, " vereda ", 8, " vreda ", 7, 0); - str_replace(buffer, &len, &changes, " chemin ", 8, " ch ", 4, 0); - str_replace(buffer, &len, &changes, " street ", 8, " st ", 4, 0); - str_replace(buffer, &len, &changes, " strand ", 8, " st ", 4, 0); - str_replace(buffer, &len, &changes, " sainte ", 8, " ste ", 5, 0); - str_replace(buffer, &len, &changes, " camino ", 8, " cno ", 5, 0); - str_replace(buffer, &len, &changes, " garden ", 8, " gdn ", 5, 0); - str_replace(buffer, &len, &changes, " follow ", 8, " folw ", 6, 0); - str_replace(buffer, &len, &changes, " estate ", 8, " est ", 5, 0); - str_replace(buffer, &len, &changes, " doktor ", 8, " d r ", 5, 0); - str_replace(buffer, &len, &changes, " subway ", 8, " sbwy ", 6, 0); - str_replace(buffer, &len, &changes, " ulitsa ", 8, " ul ", 4, 0); - str_replace(buffer, &len, &changes, " square ", 8, " sq ", 4, 0); - str_replace(buffer, &len, &changes, " towers ", 8, " twrs ", 6, 0); - str_replace(buffer, &len, &changes, "plassen ", 8, " pl ", 4, 0); - str_replace(buffer, &len, &changes, " county ", 8, " co ", 4, 0); - str_replace(buffer, &len, &changes, " brazal ", 8, " brzal ", 7, 0); - str_replace(buffer, &len, &changes, " circus ", 8, " crcs ", 6, 0); - str_replace(buffer, &len, &changes, "svingen ", 8, " sv ", 4, 0); - str_replace(buffer, &len, &changes, " rampla ", 8, " rampa ", 7, 0); - str_replace(buffer, &len, &changes, " bloque ", 8, " blque ", 7, 0); - str_replace(buffer, &len, &changes, " circle ", 8, " cir ", 5, 0); - str_replace(buffer, &len, &changes, " island ", 8, " is ", 4, 0); - str_replace(buffer, &len, &changes, " common ", 8, " comm ", 6, 0); - str_replace(buffer, &len, &changes, " ribera ", 8, " rbra ", 6, 0); - str_replace(buffer, &len, &changes, " sector ", 8, " sect ", 6, 0); - str_replace(buffer, &len, &changes, " rincon ", 8, " rcon ", 6, 0); - str_replace(buffer, &len, &changes, " van de ", 8, " vd ", 4, 0); - str_replace(buffer, &len, &changes, " corner ", 8, " cnr ", 5, 0); - str_replace(buffer, &len, &changes, " subida ", 8, " sbida ", 7, 0); - str_replace(buffer, &len, &changes, " banda ", 7, " b ", 3, 0); - str_replace(buffer, &len, &changes, " bulev ", 7, " bd ", 4, 0); - str_replace(buffer, &len, &changes, " barro ", 7, " bo ", 4, 0); - str_replace(buffer, &len, &changes, " cllon ", 7, " cjon ", 6, 0); - str_replace(buffer, &len, &changes, " p zza ", 7, " p za ", 6, 0); - str_replace(buffer, &len, &changes, " drugi ", 7, " 2 ", 3, 0); - str_replace(buffer, &len, &changes, " druga ", 7, " 2 ", 3, 0); - str_replace(buffer, &len, &changes, " placu ", 7, " pl ", 4, 0); - str_replace(buffer, &len, &changes, " aleji ", 7, " al ", 4, 0); - str_replace(buffer, &len, &changes, " aleja ", 7, " al ", 4, 0); - str_replace(buffer, &len, &changes, " aleje ", 7, " al ", 4, 0); - str_replace(buffer, &len, &changes, " stary ", 7, " st ", 4, 0); - str_replace(buffer, &len, &changes, " stara ", 7, " st ", 4, 0); - str_replace(buffer, &len, &changes, " dolny ", 7, " dln ", 5, 0); - str_replace(buffer, &len, &changes, " dolna ", 7, " dln ", 5, 0); - str_replace(buffer, &len, &changes, " gorne ", 7, " gn ", 4, 0); - str_replace(buffer, &len, &changes, " gorna ", 7, " gn ", 4, 0); - str_replace(buffer, &len, &changes, " stare ", 7, " st ", 4, 0); - str_replace(buffer, &len, &changes, " gorny ", 7, " gn ", 4, 0); - str_replace(buffer, &len, &changes, " ulicy ", 7, " ul ", 4, 0); - str_replace(buffer, &len, &changes, " ulica ", 7, " ul ", 4, 0); - str_replace(buffer, &len, &changes, " o l v ", 7, " olv ", 5, 0); - str_replace(buffer, &len, &changes, " plein ", 7, " pln ", 5, 0); - str_replace(buffer, &len, &changes, " markt ", 7, " mkt ", 5, 0); - str_replace(buffer, &len, &changes, " lange ", 7, " l ", 3, 0); - str_replace(buffer, &len, &changes, " viale ", 7, " v le ", 6, 0); - str_replace(buffer, &len, &changes, "gracht ", 7, " gr ", 4, 0); - str_replace(buffer, &len, &changes, " prins ", 7, " pr ", 4, 0); - str_replace(buffer, &len, &changes, "straat ", 7, " st ", 4, 0); - str_replace(buffer, &len, &changes, " plass ", 7, " pl ", 4, 0); - str_replace(buffer, &len, &changes, " sving ", 7, " sv ", 4, 0); - str_replace(buffer, &len, &changes, " gaten ", 7, " g ", 3, 0); - str_replace(buffer, &len, &changes, " veien ", 7, " v ", 3, 0); - str_replace(buffer, &len, &changes, " vliet ", 7, " vlt ", 5, 0); - str_replace(buffer, &len, &changes, " dolne ", 7, " dln ", 5, 0); - str_replace(buffer, &len, &changes, " b dul ", 7, " bd ", 4, 0); - str_replace(buffer, &len, &changes, " sodra ", 7, " s ", 3, 0); - str_replace(buffer, &len, &changes, " norra ", 7, " n ", 3, 0); - str_replace(buffer, &len, &changes, " gamla ", 7, " gla ", 5, 0); - str_replace(buffer, &len, &changes, " grand ", 7, " gr ", 4, 0); - str_replace(buffer, &len, &changes, " vagen ", 7, " v ", 3, 0); - str_replace(buffer, &len, &changes, " gatan ", 7, " g ", 3, 0); - str_replace(buffer, &len, &changes, " ostra ", 7, " o ", 3, 0); - str_replace(buffer, &len, &changes, "vastra ", 7, " v ", 3, 0); - str_replace(buffer, &len, &changes, " cadde ", 7, " cd ", 4, 0); - str_replace(buffer, &len, &changes, " duong ", 7, " d ", 3, 0); - str_replace(buffer, &len, &changes, " sokak ", 7, " sk ", 4, 0); - str_replace(buffer, &len, &changes, " plats ", 7, " pl ", 4, 0); - str_replace(buffer, &len, &changes, "stigen ", 7, " st ", 4, 0); - str_replace(buffer, &len, &changes, " vayla ", 7, " vla ", 5, 0); - str_replace(buffer, &len, &changes, "taival ", 7, " tvl ", 5, 0); - str_replace(buffer, &len, &changes, " sveti ", 7, " sv ", 4, 0); - str_replace(buffer, &len, &changes, " aukio ", 7, " auk ", 5, 0); - str_replace(buffer, &len, &changes, " sveta ", 7, " sv ", 4, 0); - str_replace(buffer, &len, &changes, " cesta ", 7, " c ", 3, 0); - str_replace(buffer, &len, &changes, " piata ", 7, " pta ", 5, 0); - str_replace(buffer, &len, &changes, " aleea ", 7, " al ", 4, 0); - str_replace(buffer, &len, &changes, " kaari ", 7, " kri ", 5, 0); - str_replace(buffer, &len, &changes, "penger ", 7, " pgr ", 5, 0); - str_replace(buffer, &len, &changes, " ranta ", 7, " rt ", 4, 0); - str_replace(buffer, &len, &changes, " rinne ", 7, " rn ", 4, 0); - str_replace(buffer, &len, &changes, "raitti ", 7, " r ", 3, 0); - str_replace(buffer, &len, &changes, "puisto ", 7, " ps ", 4, 0); - str_replace(buffer, &len, &changes, " polku ", 7, " p ", 3, 0); - str_replace(buffer, &len, &changes, " porta ", 7, " pta ", 5, 0); - str_replace(buffer, &len, &changes, " ponte ", 7, " p te ", 6, 0); - str_replace(buffer, &len, &changes, " paseo ", 7, " po ", 4, 0); - str_replace(buffer, &len, &changes, " fbrca ", 7, " fca ", 5, 0); - str_replace(buffer, &len, &changes, " allee ", 7, " al ", 4, 0); - str_replace(buffer, &len, &changes, " cours ", 7, " crs ", 5, 0); - str_replace(buffer, &len, &changes, "sainte ", 7, " ste ", 5, 0); - str_replace(buffer, &len, &changes, "square ", 7, " sq ", 4, 0); - str_replace(buffer, &len, &changes, " largo ", 7, " l go ", 6, 0); - str_replace(buffer, &len, &changes, " wharf ", 7, " whrf ", 6, 0); - str_replace(buffer, &len, &changes, " corte ", 7, " c te ", 6, 0); - str_replace(buffer, &len, &changes, " corso ", 7, " c so ", 6, 0); - str_replace(buffer, &len, &changes, " campo ", 7, " c po ", 6, 0); - str_replace(buffer, &len, &changes, " santa ", 7, " sta ", 5, 0); - str_replace(buffer, &len, &changes, " calle ", 7, " c ", 3, 0); - str_replace(buffer, &len, &changes, " strip ", 7, " strp ", 6, 0); - str_replace(buffer, &len, &changes, " alley ", 7, " al ", 4, 0); - str_replace(buffer, &len, &changes, " north ", 7, " n ", 3, 0); - str_replace(buffer, &len, &changes, " block ", 7, " blk ", 5, 0); - str_replace(buffer, &len, &changes, " gully ", 7, " gly ", 5, 0); - str_replace(buffer, &len, &changes, " sielo ", 7, " s ", 3, 0); - str_replace(buffer, &len, &changes, " brace ", 7, " br ", 4, 0); - str_replace(buffer, &len, &changes, " ronde ", 7, " rnde ", 6, 0); - str_replace(buffer, &len, &changes, " grove ", 7, " gr ", 4, 0); - str_replace(buffer, &len, &changes, " break ", 7, " brk ", 5, 0); - str_replace(buffer, &len, &changes, " roads ", 7, " rds ", 5, 0); - str_replace(buffer, &len, &changes, " track ", 7, " trk ", 5, 0); - str_replace(buffer, &len, &changes, " house ", 7, " ho ", 4, 0); - str_replace(buffer, &len, &changes, " trail ", 7, " trl ", 5, 0); - str_replace(buffer, &len, &changes, " mount ", 7, " mt ", 4, 0); - str_replace(buffer, &len, &changes, " cross ", 7, " crss ", 6, 0); - str_replace(buffer, &len, &changes, " beach ", 7, " bch ", 5, 0); - str_replace(buffer, &len, &changes, " point ", 7, " pt ", 4, 0); - str_replace(buffer, &len, &changes, " basin ", 7, " basn ", 6, 0); - str_replace(buffer, &len, &changes, " green ", 7, " gn ", 4, 0); - str_replace(buffer, &len, &changes, " plaza ", 7, " pl ", 4, 0); - str_replace(buffer, &len, &changes, " lille ", 7, " ll ", 4, 0); - str_replace(buffer, &len, &changes, " slope ", 7, " slpe ", 6, 0); - str_replace(buffer, &len, &changes, " placa ", 7, " pl ", 4, 0); - str_replace(buffer, &len, &changes, " place ", 7, " pl ", 4, 0); - str_replace(buffer, &len, &changes, " shunt ", 7, " shun ", 6, 0); - str_replace(buffer, &len, &changes, " saint ", 7, " st ", 4, 0); - str_replace(buffer, &len, &changes, " ulice ", 7, " ul ", 4, 0); - str_replace(buffer, &len, &changes, " amble ", 7, " ambl ", 6, 0); - str_replace(buffer, &len, &changes, " route ", 7, " rt ", 4, 0); - str_replace(buffer, &len, &changes, " sound ", 7, " snd ", 5, 0); - str_replace(buffer, &len, &changes, " store ", 7, " st ", 4, 0); - str_replace(buffer, &len, &changes, " front ", 7, " frnt ", 6, 0); - str_replace(buffer, &len, &changes, " elbow ", 7, " elb ", 5, 0); - str_replace(buffer, &len, &changes, " glade ", 7, " gl ", 4, 0); - str_replace(buffer, &len, &changes, " south ", 7, " s ", 3, 0); - str_replace(buffer, &len, &changes, " round ", 7, " rnd ", 5, 0); - str_replace(buffer, &len, &changes, " drive ", 7, " dr ", 4, 0); - str_replace(buffer, &len, &changes, " croft ", 7, " cft ", 5, 0); - str_replace(buffer, &len, &changes, " platz ", 7, " pl ", 4, 0); - str_replace(buffer, &len, &changes, " ferry ", 7, " fy ", 4, 0); - str_replace(buffer, &len, &changes, " ridge ", 7, " rdge ", 6, 0); - str_replace(buffer, &len, &changes, " tanav ", 7, " tn ", 4, 0); - str_replace(buffer, &len, &changes, " banan ", 7, " ba ", 4, 0); - str_replace(buffer, &len, &changes, " quays ", 7, " qys ", 5, 0); - str_replace(buffer, &len, &changes, " sankt ", 7, " st ", 4, 0); - str_replace(buffer, &len, &changes, " vkhod ", 7, " vkh ", 5, 0); - str_replace(buffer, &len, &changes, " chase ", 7, " ch ", 4, 0); - str_replace(buffer, &len, &changes, " vista ", 7, " vsta ", 6, 0); - str_replace(buffer, &len, &changes, " rhein ", 7, " rh ", 4, 0); - str_replace(buffer, &len, &changes, " court ", 7, " ct ", 4, 0); - str_replace(buffer, &len, &changes, "brucke ", 7, " br ", 4, 0); - str_replace(buffer, &len, &changes, " upper ", 7, " up ", 4, 0); - str_replace(buffer, &len, &changes, " river ", 7, " r ", 3, 0); - str_replace(buffer, &len, &changes, " range ", 7, " rnge ", 6, 0); - str_replace(buffer, &len, &changes, " lower ", 7, " lr ", 4, 0); - str_replace(buffer, &len, &changes, " kalea ", 7, " k ", 3, 0); - str_replace(buffer, &len, &changes, " crest ", 7, " crst ", 6, 0); - str_replace(buffer, &len, &changes, " obere ", 7, " o ", 3, 0); - str_replace(buffer, &len, &changes, " manor ", 7, " mnr ", 5, 0); - str_replace(buffer, &len, &changes, " byway ", 7, " bywy ", 6, 0); - str_replace(buffer, &len, &changes, " reach ", 7, " rch ", 5, 0); - str_replace(buffer, &len, &changes, " copse ", 7, " cps ", 5, 0); - str_replace(buffer, &len, &changes, "quelle ", 7, " qu ", 4, 0); - str_replace(buffer, &len, &changes, " creek ", 7, " cr ", 4, 0); - str_replace(buffer, &len, &changes, " close ", 7, " c ", 3, 0); - str_replace(buffer, &len, &changes, " fort ", 6, " ft ", 4, 0); - str_replace(buffer, &len, &changes, " apch ", 6, " app ", 5, 0); - str_replace(buffer, &len, &changes, " mont ", 6, " mt ", 4, 0); - str_replace(buffer, &len, &changes, " bdul ", 6, " bd ", 4, 0); - str_replace(buffer, &len, &changes, "saint ", 6, " st ", 4, 0); - str_replace(buffer, &len, &changes, " back ", 6, " bk ", 4, 0); - str_replace(buffer, &len, &changes, " c le ", 6, " c ", 3, 0); - str_replace(buffer, &len, &changes, "place ", 6, " pl ", 4, 0); - str_replace(buffer, &len, &changes, " frwy ", 6, " fwy ", 5, 0); - str_replace(buffer, &len, &changes, " quai ", 6, " qu ", 4, 0); - str_replace(buffer, &len, &changes, " ally ", 6, " al ", 4, 0); - str_replace(buffer, &len, &changes, " m te ", 6, " mt ", 4, 0); - str_replace(buffer, &len, &changes, " lane ", 6, " ln ", 4, 0); - str_replace(buffer, &len, &changes, "aukio ", 6, " auk ", 5, 0); - str_replace(buffer, &len, &changes, " loop ", 6, " lp ", 4, 0); - str_replace(buffer, &len, &changes, " line ", 6, " ln ", 4, 0); - str_replace(buffer, &len, &changes, " alue ", 6, " al ", 4, 0); - str_replace(buffer, &len, &changes, " link ", 6, " lk ", 4, 0); - str_replace(buffer, &len, &changes, " glde ", 6, " gl ", 4, 0); - str_replace(buffer, &len, &changes, " alea ", 6, " al ", 4, 0); - str_replace(buffer, &len, &changes, " gate ", 6, " g ", 3, 0); - str_replace(buffer, &len, &changes, " intr ", 6, " int ", 5, 0); - str_replace(buffer, &len, &changes, " gdns ", 6, " gdn ", 5, 0); - str_replace(buffer, &len, &changes, " hird ", 6, " hrd ", 5, 0); - str_replace(buffer, &len, &changes, " varf ", 6, " vf ", 4, 0); - str_replace(buffer, &len, &changes, " virf ", 6, " vf ", 4, 0); - str_replace(buffer, &len, &changes, " hgts ", 6, " hts ", 5, 0); - str_replace(buffer, &len, &changes, " expy ", 6, " exp ", 5, 0); - str_replace(buffer, &len, &changes, "markt ", 6, " mkt ", 5, 0); - str_replace(buffer, &len, &changes, " bypa ", 6, " byp ", 5, 0); - str_replace(buffer, &len, &changes, "o l v ", 6, " olv ", 5, 0); - str_replace(buffer, &len, &changes, " cres ", 6, " cr ", 4, 0); - str_replace(buffer, &len, &changes, " bdwy ", 6, " bway ", 6, 0); - str_replace(buffer, &len, &changes, " csac ", 6, " cds ", 5, 0); - str_replace(buffer, &len, &changes, " nowy ", 6, " n ", 3, 0); - str_replace(buffer, &len, &changes, " laan ", 6, " ln ", 4, 0); - str_replace(buffer, &len, &changes, " crsg ", 6, " xing ", 6, 0); - str_replace(buffer, &len, &changes, "vliet ", 6, " vlt ", 5, 0); - str_replace(buffer, &len, &changes, " city ", 6, " cty ", 5, 0); - str_replace(buffer, &len, &changes, "sving ", 6, " sv ", 4, 0); - str_replace(buffer, &len, &changes, "plass ", 6, " pl ", 4, 0); - str_replace(buffer, &len, &changes, "gaten ", 6, " g ", 3, 0); - str_replace(buffer, &len, &changes, "veien ", 6, " v ", 3, 0); - str_replace(buffer, &len, &changes, " gata ", 6, " g ", 3, 0); - str_replace(buffer, &len, &changes, " sint ", 6, " st ", 4, 0); - str_replace(buffer, &len, &changes, " caus ", 6, " cway ", 6, 0); - str_replace(buffer, &len, &changes, " cove ", 6, " cv ", 4, 0); - str_replace(buffer, &len, &changes, "plein ", 6, " pln ", 5, 0); - str_replace(buffer, &len, &changes, " cswy ", 6, " cway ", 6, 0); - str_replace(buffer, &len, &changes, " plac ", 6, " pl ", 4, 0); - str_replace(buffer, &len, &changes, " nowa ", 6, " n ", 3, 0); - str_replace(buffer, &len, &changes, " kolo ", 6, " k ", 3, 0); - str_replace(buffer, &len, &changes, " katu ", 6, " k ", 3, 0); - str_replace(buffer, &len, &changes, " duze ", 6, " dz ", 4, 0); - str_replace(buffer, &len, &changes, " blvd ", 6, " bd ", 4, 0); - str_replace(buffer, &len, &changes, " p ta ", 6, " pta ", 5, 0); - str_replace(buffer, &len, &changes, " maly ", 6, " ml ", 4, 0); - str_replace(buffer, &len, &changes, " mala ", 6, " ml ", 4, 0); - str_replace(buffer, &len, &changes, " bdge ", 6, " bri ", 5, 0); - str_replace(buffer, &len, &changes, " nowe ", 6, " n ", 3, 0); - str_replace(buffer, &len, &changes, " brdg ", 6, " bri ", 5, 0); - str_replace(buffer, &len, &changes, " male ", 6, " ml ", 4, 0); - str_replace(buffer, &len, &changes, " drwy ", 6, " dvwy ", 6, 0); - str_replace(buffer, &len, &changes, " duza ", 6, " dz ", 4, 0); - str_replace(buffer, &len, &changes, " utca ", 6, " u ", 3, 0); - str_replace(buffer, &len, &changes, " east ", 6, " e ", 3, 0); - str_replace(buffer, &len, &changes, " duzy ", 6, " dz ", 4, 0); - str_replace(buffer, &len, &changes, "kaari ", 6, " kri ", 5, 0); - str_replace(buffer, &len, &changes, " quan ", 6, " q ", 3, 0); - str_replace(buffer, &len, &changes, " svwy ", 6, " swy ", 5, 0); - str_replace(buffer, &len, &changes, " shwy ", 6, " sh ", 4, 0); - str_replace(buffer, &len, &changes, " road ", 6, " rd ", 4, 0); - str_replace(buffer, &len, &changes, "sankt ", 6, " st ", 4, 0); - str_replace(buffer, &len, &changes, " quay ", 6, " qy ", 4, 0); - str_replace(buffer, &len, &changes, "plats ", 6, " pl ", 4, 0); - str_replace(buffer, &len, &changes, " rise ", 6, " ri ", 4, 0); - str_replace(buffer, &len, &changes, " berg ", 6, " bg ", 4, 0); - str_replace(buffer, &len, &changes, " tcty ", 6, " tct ", 5, 0); - str_replace(buffer, &len, &changes, " viad ", 6, " via ", 5, 0); - str_replace(buffer, &len, &changes, " view ", 6, " vw ", 4, 0); - str_replace(buffer, &len, &changes, " vdct ", 6, " via ", 5, 0); - str_replace(buffer, &len, &changes, " vale ", 6, " v ", 3, 0); - str_replace(buffer, &len, &changes, " avda ", 6, " av ", 4, 0); - str_replace(buffer, &len, &changes, " grad ", 6, " ghr ", 5, 0); - str_replace(buffer, &len, &changes, " walk ", 6, " wlk ", 5, 0); - str_replace(buffer, &len, &changes, " west ", 6, " w ", 3, 0); - str_replace(buffer, &len, &changes, " yard ", 6, " yd ", 4, 0); - str_replace(buffer, &len, &changes, " blok ", 6, " bl ", 4, 0); - str_replace(buffer, &len, &changes, " terr ", 6, " ter ", 5, 0); - str_replace(buffer, &len, &changes, " cmno ", 6, " cno ", 5, 0); - str_replace(buffer, &len, &changes, " stra ", 6, " st ", 4, 0); - str_replace(buffer, &len, &changes, " thfr ", 6, " thor ", 6, 0); - str_replace(buffer, &len, &changes, " turn ", 6, " tn ", 4, 0); - str_replace(buffer, &len, &changes, " tpke ", 6, " tpk ", 5, 0); - str_replace(buffer, &len, &changes, " burg ", 6, " bg ", 4, 0); - str_replace(buffer, &len, &changes, "vayla ", 6, " vla ", 5, 0); - str_replace(buffer, &len, &changes, "vagen ", 6, " v ", 3, 0); - str_replace(buffer, &len, &changes, " tori ", 6, " tr ", 4, 0); - str_replace(buffer, &len, &changes, "gatan ", 6, " g ", 3, 0); - str_replace(buffer, &len, &changes, "grand ", 6, " gr ", 4, 0); - str_replace(buffer, &len, &changes, " pass ", 6, " ps ", 4, 0); - str_replace(buffer, &len, &changes, " pkwy ", 6, " pwy ", 5, 0); - str_replace(buffer, &len, &changes, " park ", 6, " pk ", 4, 0); - str_replace(buffer, &len, &changes, "rinne ", 6, " rn ", 4, 0); - str_replace(buffer, &len, &changes, " mtwy ", 6, " mwy ", 5, 0); - str_replace(buffer, &len, &changes, " mndr ", 6, " mr ", 4, 0); - str_replace(buffer, &len, &changes, " kyla ", 6, " kl ", 4, 0); - str_replace(buffer, &len, &changes, " kuja ", 6, " kj ", 4, 0); - str_replace(buffer, &len, &changes, "platz ", 6, " pl ", 4, 0); - str_replace(buffer, &len, &changes, "ranta ", 6, " rt ", 4, 0); - str_replace(buffer, &len, &changes, " mile ", 6, " mi ", 4, 0); - str_replace(buffer, &len, &changes, " pfad ", 6, " p ", 3, 0); - str_replace(buffer, &len, &changes, " mews ", 6, " m ", 3, 0); - str_replace(buffer, &len, &changes, "polku ", 6, " p ", 3, 0); - str_replace(buffer, &len, &changes, " psge ", 6, " ps ", 4, 0); - str_replace(buffer, &len, &changes, " plza ", 6, " pl ", 4, 0); - str_replace(buffer, &len, &changes, "ostra ", 6, " o ", 3, 0); - str_replace(buffer, &len, &changes, "gamla ", 6, " gla ", 5, 0); - str_replace(buffer, &len, &changes, " stig ", 6, " st ", 4, 0); - str_replace(buffer, &len, &changes, "norra ", 6, " n ", 3, 0); - str_replace(buffer, &len, &changes, "sodra ", 6, " s ", 3, 0); - str_replace(buffer, &len, &changes, " pike ", 6, " pk ", 4, 0); - str_replace(buffer, &len, &changes, " dorf ", 6, " df ", 4, 0); - str_replace(buffer, &len, &changes, " piaz ", 6, " p za ", 6, 0); - str_replace(buffer, &len, &changes, " phwy ", 6, " pway ", 6, 0); - str_replace(buffer, &len, &changes, "pfad ", 5, " p ", 3, 0); - str_replace(buffer, &len, &changes, " mnt ", 5, " mt ", 4, 0); - str_replace(buffer, &len, &changes, "gata ", 5, " g ", 3, 0); - str_replace(buffer, &len, &changes, " bhf ", 5, " bf ", 4, 0); - str_replace(buffer, &len, &changes, " bad ", 5, " b ", 3, 0); - str_replace(buffer, &len, &changes, "gate ", 5, " g ", 3, 0); - str_replace(buffer, &len, &changes, " zum ", 5, " z ", 3, 0); - str_replace(buffer, &len, &changes, "stig ", 5, " st ", 4, 0); - str_replace(buffer, &len, &changes, " blv ", 5, " bd ", 4, 0); - str_replace(buffer, &len, &changes, "kuja ", 5, " kj ", 4, 0); - str_replace(buffer, &len, &changes, " bul ", 5, " bd ", 4, 0); - str_replace(buffer, &len, &changes, " str ", 5, " st ", 4, 0); - str_replace(buffer, &len, &changes, "alue ", 5, " al ", 4, 0); - str_replace(buffer, &len, &changes, " cen ", 5, " ctr ", 5, 0); - str_replace(buffer, &len, &changes, " ave ", 5, " av ", 4, 0); - str_replace(buffer, &len, &changes, "kyla ", 5, " kl ", 4, 0); - str_replace(buffer, &len, &changes, " ale ", 5, " al ", 4, 0); - str_replace(buffer, &len, &changes, " spl ", 5, " sp ", 4, 0); - str_replace(buffer, &len, &changes, " all ", 5, " al ", 4, 0); - str_replace(buffer, &len, &changes, " k s ", 5, " ks ", 4, 0); - str_replace(buffer, &len, &changes, " aly ", 5, " al ", 4, 0); - str_replace(buffer, &len, &changes, "dorf ", 5, " df ", 4, 0); - str_replace(buffer, &len, &changes, " bvd ", 5, " bd ", 4, 0); - str_replace(buffer, &len, &changes, " vag ", 5, " v ", 3, 0); - str_replace(buffer, &len, &changes, " iii ", 5, " 3 ", 3, 0); - str_replace(buffer, &len, &changes, " tie ", 5, " t ", 3, 0); - str_replace(buffer, &len, &changes, " sok ", 5, " sk ", 4, 0); - str_replace(buffer, &len, &changes, "burg ", 5, " bg ", 4, 0); - str_replace(buffer, &len, &changes, "katu ", 5, " k ", 3, 0); - str_replace(buffer, &len, &changes, "berg ", 5, " bg ", 4, 0); - str_replace(buffer, &len, &changes, "tori ", 5, " tr ", 4, 0); - str_replace(buffer, &len, &changes, " kte ", 5, " k ", 3, 0); - str_replace(buffer, &len, &changes, " gro ", 5, " gr ", 4, 0); - str_replace(buffer, &len, &changes, " grn ", 5, " gn ", 4, 0); - str_replace(buffer, &len, &changes, " gld ", 5, " gl ", 4, 0); - str_replace(buffer, &len, &changes, " san ", 5, " s ", 3, 0); - str_replace(buffer, &len, &changes, " hse ", 5, " ho ", 4, 0); - str_replace(buffer, &len, &changes, " gte ", 5, " g ", 3, 0); - str_replace(buffer, &len, &changes, " rte ", 5, " rt ", 4, 0); - str_replace(buffer, &len, &changes, " rue ", 5, " r ", 3, 0); - str_replace(buffer, &len, &changes, " che ", 5, " ch ", 4, 0); - str_replace(buffer, &len, &changes, " pas ", 5, " ps ", 4, 0); - str_replace(buffer, &len, &changes, " plz ", 5, " pl ", 4, 0); - str_replace(buffer, &len, &changes, " pnt ", 5, " pt ", 4, 0); - str_replace(buffer, &len, &changes, " pky ", 5, " pwy ", 5, 0); - str_replace(buffer, &len, &changes, " pza ", 5, " pl ", 4, 0); - str_replace(buffer, &len, &changes, " rvr ", 5, " r ", 3, 0); - str_replace(buffer, &len, &changes, " riv ", 5, " r ", 3, 0); - str_replace(buffer, &len, &changes, " lit ", 5, " lt ", 4, 0); - str_replace(buffer, &len, &changes, " p k ", 5, " pk ", 4, 0); - str_replace(buffer, &len, &changes, " lwr ", 5, " lr ", 4, 0); - str_replace(buffer, &len, &changes, " low ", 5, " lr ", 4, 0); - str_replace(buffer, &len, &changes, " sth ", 5, " s ", 3, 0); - str_replace(buffer, &len, &changes, " crk ", 5, " cr ", 4, 0); - str_replace(buffer, &len, &changes, "pres ", 5, " pres ", 6, 1); - str_replace(buffer, &len, &changes, "laan ", 5, " ln ", 4, 0); - str_replace(buffer, &len, &changes, " bda ", 5, " b ", 3, 0); - str_replace(buffer, &len, &changes, " vei ", 5, " v ", 3, 0); - str_replace(buffer, &len, &changes, " via ", 5, " v ", 3, 0); - str_replace(buffer, &len, &changes, " way ", 5, " wy ", 4, 0); - str_replace(buffer, &len, &changes, " upr ", 5, " up ", 4, 0); - str_replace(buffer, &len, &changes, " avd ", 5, " av ", 4, 0); - str_replace(buffer, &len, &changes, " crt ", 5, " ct ", 4, 0); - str_replace(buffer, &len, &changes, "stwg ", 5, " stwg ", 6, 1); - str_replace(buffer, &len, &changes, "sint ", 5, " st ", 4, 0); - str_replace(buffer, &len, &changes, " v d ", 5, " vd ", 4, 0); - str_replace(buffer, &len, &changes, " van ", 5, " v ", 3, 0); - str_replace(buffer, &len, &changes, " drv ", 5, " dr ", 4, 0); - str_replace(buffer, &len, &changes, " tce ", 5, " ter ", 5, 0); - str_replace(buffer, &len, &changes, " va ", 4, " v ", 3, 0); - str_replace(buffer, &len, &changes, " oa ", 4, " o ", 3, 0); - str_replace(buffer, &len, &changes, " sa ", 4, " s ", 3, 0); - str_replace(buffer, &len, &changes, " na ", 4, " n ", 3, 0); - str_replace(buffer, &len, &changes, "bgm ", 4, " bgm ", 5, 1); - str_replace(buffer, &len, &changes, " nw ", 4, " n ", 3, 0); - str_replace(buffer, &len, &changes, "vag ", 4, " v ", 3, 0); - str_replace(buffer, &len, &changes, " im ", 4, " 1 ", 3, 0); - str_replace(buffer, &len, &changes, "vla ", 4, " vla ", 5, 1); - str_replace(buffer, &len, &changes, "gla ", 4, " gla ", 5, 1); - str_replace(buffer, &len, &changes, " am ", 4, " a ", 3, 0); - str_replace(buffer, &len, &changes, " ph ", 4, " p ", 3, 0); - str_replace(buffer, &len, &changes, "rue ", 4, " r ", 3, 0); - str_replace(buffer, &len, &changes, " ga ", 4, " g ", 3, 0); - str_replace(buffer, &len, &changes, "ste ", 4, " ste ", 5, 1); - str_replace(buffer, &len, &changes, "str ", 4, " st ", 4, 0); - str_replace(buffer, &len, &changes, " cl ", 4, " c ", 3, 0); - str_replace(buffer, &len, &changes, " vn ", 4, " v ", 3, 0); - str_replace(buffer, &len, &changes, " gt ", 4, " g ", 3, 0); - str_replace(buffer, &len, &changes, "vei ", 4, " v ", 3, 0); - str_replace(buffer, &len, &changes, "vlt ", 4, " vlt ", 5, 1); - str_replace(buffer, &len, &changes, " ce ", 4, " cv ", 4, 0); - str_replace(buffer, &len, &changes, " ii ", 4, " 2 ", 3, 0); - str_replace(buffer, &len, &changes, "pln ", 4, " pln ", 5, 1); - str_replace(buffer, &len, &changes, "olv ", 4, " olv ", 5, 1); - str_replace(buffer, &len, &changes, "mkt ", 4, " mkt ", 5, 1); - str_replace(buffer, &len, &changes, "tvl ", 4, " tvl ", 5, 1); - str_replace(buffer, &len, &changes, " ob ", 4, " o ", 3, 0); - str_replace(buffer, &len, &changes, "pgr ", 4, " pgr ", 5, 1); - str_replace(buffer, &len, &changes, " in ", 4, " 1 ", 3, 0); - str_replace(buffer, &len, &changes, " mw ", 4, " m ", 3, 0); - str_replace(buffer, &len, &changes, "kri ", 4, " kri ", 5, 1); - str_replace(buffer, &len, &changes, "pko ", 4, " pko ", 5, 1); - str_replace(buffer, &len, &changes, "auk ", 4, " auk ", 5, 1); - str_replace(buffer, &len, &changes, "tie ", 4, " t ", 3, 0); - str_replace(buffer, &len, &changes, " i ", 3, " 1 ", 3, 0); diff --git a/module/utfasciitable.h b/module/utfasciitable.h deleted file mode 100644 index a099ed6b..00000000 --- a/module/utfasciitable.h +++ /dev/null @@ -1,10 +0,0 @@ -/** - * SPDX-License-Identifier: GPL-2.0-only - * - * This file is part of Nominatim. (https://nominatim.org) - * - * Copyright (C) 2022 by the Nominatim developer community. - * For a full list of authors see the git log. - */ -#define UTFASCII " \x00""\x01"" \x01""0\x01""1\x01""2\x01""3\x01""4\x01""5\x01""6\x01""7\x01""8\x01""9\x01""a\x01""b\x01""c\x01""d\x01""e\x01""f\x01""g\x01""h\x01""i\x01""j\x01""k\x01""l\x01""m\x01""n\x01""o\x01""p\x01""q\x01""r\x01""s\x01""t\x01""u\x01""v\x01""w\x01""x\x01""y\x01""z\x02""ps\x02""ss\x03""deg\x01""-\x02""14\x02""12\x02""34\x02""ae\x02""th\x02""ij\x02""ng\x02""oe\x02""hv\x02""oi\x02""yr\x02""sh\x02""zh\x02""ts\x02""dz\x02""lj\x02""nj\x02""ou\x02""db\x02""qp\x04""stop\x02""lz\x02""tc\x02""fn\x02""ls\x02""ww\x0a""extra-high\x04""high\x03""mid\x04""tone\x09""extra-low\x03""yin\x04""yang\x04""down\x02""up\x04""left\x05""right\x04""ring\x06""middle\x05""tilde\x06""raised\x05""begin\x03""end\x05""shelf\x05""below\x04""heta\x05""sampi\x0a""pamphylian\x02""ks\x02""ph\x02""kh\x05""koppa\x02""st\x02""sp\x02""ch\x02""ti\x03""sho\x03""san\x02""ie\x02""io\x02""dj\x02""gj\x02""yi\x03""tsh\x02""kj\x03""dzh\x04""shch\x04""hard\x02""iu\x02""ia\x02""gh\x02""ot\x04""1000\x06""100000\x07""1000000\x03""tts\x02""el\x02""en\x02""em\x08""palochka\x03""ghe\x02""ha\x02""de\x03""dje\x03""zje\x04""dzje\x03""lje\x03""nje\x03""sje\x03""tje\x02""ze\x03""lha\x03""rha\x03""yae\x02""qa\x02""we\x05""aleut\x02""rh\x02""ew\x04""alef\x04""ayin\x02""oy\x05""sanah\x05""safha\x05""misra\x0b""sallallahou\x06""alayhe\x0b""rahmatullah\x04""radi\x09""takhallus\x05""hamza\x03""teh\x02""dh\x03""ain\x05""keheh\x05""farsi\x02""an\x02""un\x02""in\x05""small\x03""dot\x03""beh\x03""qaf\x02""tt\x03""tth\x02""bh\x03""hah\x02""ny\x02""dy\x03""cch\x02""dd\x02""dt\x03""ddh\x02""rr\x02""hy\x02""yu\x03""yeh\x03""sad\x03""dal\x03""reh\x06""sindhi\x03""heh\x05""alaph\x02""yh\x07""persian\x07""sogdian\x04""seen\x03""feh\x04""meem\x04""noon\x03""lam\x03""waw\x03""kaf\x05""alifu\x02""hh\x04""ainu\x02""aa\x02""ee\x02""oo\x02""ey\x02""oa\x03""naa\x0a""dagbasinna\x02""ba\x02""pa\x02""ta\x02""ja\x03""cha\x02""da\x02""ra\x03""rra\x02""sa\x03""gba\x02""fa\x02""ka\x02""la\x02""na\x02""ma\x03""nya\x02""wa\x02""ya\x04""jona\x0b""candrabindu\x08""anusvara\x07""visarga\x02""ii\x02""uu\x06""candra\x02""ai\x02""au\x03""kha\x02""ga\x03""gha\x03""nga\x02""ca\x03""jha\x03""tta\x04""ttha\x03""dda\x04""ddha\x03""nna\x03""tha\x03""dha\x04""nnna\x03""pha\x03""bha\x03""lla\x04""llla\x02""va\x03""sha\x03""ssa\x05""nukta\x08""avagraha\x06""virama\x06""udatta\x08""anudatta\x04""khha\x04""ghha\x02""za\x05""dddha\x03""yya\x02""ll\x03""gga\x03""jja\x04""ddda\x03""bba\x06""khanda\x02""on\x02""jh\x02""nn\x03""nnn\x03""lll\x03""aum\x05""udaat\x03""khh\x03""ghh\x04""dddh\x02""yy\x06""yakash\x02""rs\x02""bb\x03""geo\x03""tsa\x03""dza\x05""tuumu\x0b""jihvamuliya\x0b""upadhmaniya\x06""chillu\x08""iruyanna\x06""eyanna\x0a""alpapraana\x07""yayanna\x07""rayanna\x07""dantaja\x09""muurdhaja\x0a""aela-pilla\x05""ketti\x04""diga\x0c""gaetta-pilla\x07""kombuva\x05""kombu\x0b""gayanukitta\x02""ko\x03""kho\x02""yo\x03""tho\x02""so\x07""phinthu\x0b""lakkhangyao\x03""mai\x08""nikhahit\x08""yamakkan\x07""fongman\x0a""angkhankhu\x06""khomut\x03""aae\x02""no\x03""nng\x03""jny\x03""nyj\x04""nndd\x02""nd\x02""mb\x02""lo\x02""om\x04""half\x02""am\x02""ue\x03""uue\x03""yar\x02""ao\x04""tsha\x04""dzha\x03""zha\x02""-a\x04""kssa\x0a""fixed-form\x03""kka\x04""rjes\x04""rnam\x03""sna\x03""lci\x04""mchu\x03""gru\x02""ei\x02""ay\x04""rdel\x02""hn\x02""hm\x04""nnya\x04""shan\x03""mon\x04""tall\x04""asat\x06""medial\x05""great\x03""ssh\x03""kss\x07""western\x07""eastern\x05""rumai\x03""rae\x04""char\x04""jhan\x03""hae\x02""he\x03""hie\x03""har\x03""hoe\x03""ban\x03""gan\x03""don\x03""vin\x03""zen\x03""tan\x03""kan\x03""las\x03""man\x03""nar\x03""par\x04""zhar\x03""tar\x04""phar\x04""khar\x04""ghan\x03""qar\x04""shin\x04""chin\x03""can\x03""jil\x03""cil\x03""xan\x02""fi\x02""yn\x05""elifi\x08""georgian\x03""nny\x06""filler\x02""eo\x03""yeo\x02""ye\x03""wae\x03""weo\x02""wi\x02""eu\x03""a-o\x03""a-u\x04""ya-o\x05""ya-yo\x04""eo-o\x04""eo-u\x05""eo-eu\x05""yeo-o\x05""yeo-u\x04""o-eo\x03""o-e\x04""o-ye\x03""o-o\x03""o-u\x05""yo-ya\x06""yo-yae\x06""yo-yeo\x04""yo-o\x04""yo-i\x03""u-a\x04""u-ae\x07""u-eo-eu\x04""u-ye\x03""u-u\x04""yu-a\x05""yu-eo\x04""yu-e\x06""yu-yeo\x05""yu-ye\x04""yu-u\x04""yu-i\x04""eu-u\x05""eu-eu\x04""yi-u\x03""i-a\x04""i-ya\x03""i-o\x03""i-u\x04""i-eu\x07""i-araea\x05""araea\x08""araea-eo\x02""xh\x0c""nieun-tikeut\x0a""nieun-sios\x0d""nieun-pansios\x0d""nieun-thieuth\x0d""tikeut-kiyeok\x0c""tikeut-rieul\x11""rieul-kiyeok-sios\x0b""rieul-nieun\x0c""rieul-tikeut\x12""rieul-tikeut-hieuh\x0b""hieuh-mieum\x0b""hieuh-pieup\x0b""yeorinhieuh\x02""gg\x03""laa\x02""jj\x02""nb\x02""dg\x02""rn\x02""mn\x02""bg\x02""bn\x03""sza\x02""bs\x03""bsg\x03""bst\x03""bsb\x03""bss\x03""bsj\x02""bj\x02""bc\x02""bt\x02""bp\x03""bbn\x02""sg\x02""sn\x02""sd\x02""sr\x02""sm\x02""sb\x03""sbg\x03""sss\x02""sj\x02""sc\x02""sk\x04""shee\x03""she\x04""shwa\x03""qoa\x03""qha\x03""qhu\x02""ck\x04""qhee\x03""qhe\x02""pb\x02""pn\x04""qhwi\x05""qhwaa\x05""qhwee\x04""qhwe\x04""u-eo\x03""u-i\x02""gs\x02""nh\x02""lg\x02""lm\x02""lb\x02""lt\x02""lp\x02""lh\x02""gl\x03""gsg\x02""ns\x02""nz\x02""nt\x02""tl\x03""lgs\x02""ln\x02""ld\x03""lth\x03""lmg\x03""lms\x03""lbs\x03""lbh\x03""rnp\x03""lss\x02""lk\x02""lq\x02""mg\x02""ml\x02""ms\x03""mss\x02""mz\x02""mc\x02""mh\x02""bl\x02""sl\x02""hl\x02""hb\x03""ddi\x04""ddaa\x04""ddee\x03""dde\x03""ddo\x04""ddwa\x02""hu\x02""hi\x03""haa\x03""hee\x02""ho\x03""jwa\x02""lu\x02""li\x03""lee\x02""le\x03""lwa\x03""hha\x03""hhu\x03""hhi\x04""hhaa\x04""hhee\x03""hhe\x03""hho\x04""hhwa\x02""mu\x02""mi\x03""maa\x03""mee\x02""me\x02""mo\x03""mwa\x03""szu\x03""szi\x04""szaa\x04""szee\x03""sze\x03""szo\x04""szwa\x02""ru\x02""ri\x03""raa\x03""ree\x02""re\x02""ro\x03""rwa\x02""su\x02""si\x03""saa\x03""see\x02""se\x03""swa\x03""shu\x03""shi\x04""shaa\x02""qu\x02""qi\x03""qaa\x03""qee\x02""qe\x02""qo\x04""tzoa\x03""qwa\x02""fu\x03""qwi\x04""qwaa\x04""qwee\x03""qwe\x02""fo\x03""fwa\x03""qhi\x04""qhaa\x03""qho\x03""pwa\x04""qhwa\x03""mya\x02""bu\x02""bi\x03""baa\x03""bee\x02""be\x02""bo\x03""bwa\x02""vu\x02""vi\x03""vaa\x03""vee\x02""ve\x02""vo\x03""vwa\x02""tu\x03""taa\x03""tee\x02""te\x02""to\x03""twa\x02""cu\x02""ci\x03""caa\x03""cee\x02""ce\x02""co\x03""cwa\x02""xa\x02""xu\x02""xi\x03""xaa\x03""xee\x02""xe\x02""xo\x03""bwe\x03""xwa\x03""fwi\x03""xwi\x04""xwaa\x04""xwee\x03""xwe\x04""pwee\x03""pwe\x02""nu\x02""ni\x03""nee\x02""ne\x03""nwa\x03""nyu\x03""nyi\x04""nyaa\x04""nyee\x03""nye\x03""nyo\x04""nywa\x02""ku\x02""ki\x03""kaa\x03""kee\x02""ke\x03""kwa\x03""kwi\x04""kwaa\x04""kwee\x03""kwe\x03""kxa\x03""kxu\x03""kxi\x04""kxaa\x04""kxee\x03""kxe\x03""kxo\x03""hna\x04""kxwa\x04""kxwi\x05""kxwaa\x05""kxwee\x04""kxwe\x03""qua\x03""que\x02""wu\x03""waa\x03""wee\x02""wo\x02""sv\x02""di\x02""zu\x02""zi\x03""zaa\x03""zee\x02""zo\x03""zwa\x03""zhu\x03""zhi\x04""zhaa\x04""zhee\x03""zhe\x03""zho\x04""zhwa\x03""yaa\x03""yee\x02""du\x03""daa\x03""dee\x02""do\x03""dwa\x03""ddu\x02""ju\x02""ji\x03""jaa\x03""jee\x02""je\x02""jo\x02""gu\x02""gi\x03""gaa\x03""gee\x02""ge\x02""go\x03""gwa\x03""gwi\x04""gwaa\x04""gwee\x03""gwe\x03""ggu\x03""ggi\x04""ggaa\x04""ggee\x03""gge\x03""ggo\x03""thu\x03""thi\x04""thaa\x04""thee\x03""the\x04""thwa\x03""chu\x03""chi\x04""chaa\x04""chee\x03""che\x03""cho\x04""chwa\x03""phu\x03""phi\x04""phaa\x04""phee\x03""phe\x03""pho\x04""phwa\x03""tsu\x03""tsi\x04""tsaa\x04""tsee\x03""tse\x03""tso\x04""tswa\x03""tza\x03""tzu\x03""tzi\x04""tzaa\x04""tzee\x03""tze\x03""tzo\x03""faa\x03""fee\x02""fe\x02""pu\x02""pi\x03""paa\x03""pee\x02""pe\x02""po\x03""rya\x03""fya\x02""gv\x02""lv\x03""nah\x02""nv\x03""qui\x03""quo\x03""quu\x03""quv\x02""dv\x03""dla\x03""tla\x03""tle\x03""tli\x03""tlo\x03""tlu\x03""tlv\x03""tsv\x02""wv\x02""yv\x03""aai\x03""wii\x03""woo\x04""paai\x03""pii\x03""poo\x03""pwi\x04""pwii\x03""pwo\x04""pwoo\x04""pwaa\x04""taai\x03""tii\x03""too\x03""twe\x03""twi\x04""twii\x03""two\x04""twoo\x04""twaa\x03""tte\x03""tti\x03""tto\x04""kaai\x03""kii\x03""koo\x04""kwii\x03""kwo\x04""kwoo\x02""kw\x03""keh\x03""kih\x03""koh\x03""kah\x04""caai\x03""cii\x03""coo\x03""cwe\x03""cwi\x04""cwii\x03""cwo\x04""cwoo\x04""cwaa\x04""maai\x03""mii\x03""moo\x03""mwe\x03""mwi\x04""mwii\x03""mwo\x04""mwoo\x04""mwaa\x04""naai\x03""nii\x03""noo\x03""nwe\x04""nwaa\x04""laai\x03""lii\x03""loo\x03""lwe\x03""lwi\x04""lwii\x03""lwo\x04""lwoo\x04""lwaa\x04""saai\x03""sii\x03""soo\x03""swe\x03""swi\x04""swii\x03""swo\x04""swoo\x04""swaa\x02""sw\x03""skw\x04""spwa\x04""stwa\x04""skwa\x04""scwa\x04""shii\x04""shoo\x04""shwe\x04""shwi\x05""shwii\x04""shwo\x05""shwoo\x05""shwaa\x04""yaai\x03""yii\x03""yoo\x03""ywe\x03""ywi\x04""ywii\x03""ywo\x04""ywoo\x03""ywa\x04""ywaa\x04""raai\x03""rii\x03""roo\x04""rwaa\x04""faai\x03""fii\x03""foo\x04""fwaa\x04""thii\x04""thoo\x05""thwaa\x04""tthe\x04""tthi\x04""ttho\x03""tye\x03""tyi\x03""tyo\x03""tya\x03""hii\x03""hoo\x02""hk\x04""qaai\x03""qii\x03""qoo\x04""tlhe\x04""tlhi\x04""tlho\x04""tlha\x05""ngaai\x03""ngi\x04""ngii\x03""ngo\x04""ngoo\x04""ngaa\x03""lhi\x04""lhii\x03""lho\x04""lhoo\x04""lhaa\x03""ghu\x03""gho\x04""ghee\x03""ghi\x03""hwu\x03""hwo\x03""hwe\x04""hwee\x03""hwi\x03""hwa\x03""ttu\x04""ttee\x03""khu\x03""khe\x04""khee\x03""khi\x03""kku\x03""kko\x03""kke\x04""kkee\x03""kki\x02""kk\x03""jju\x03""jjo\x03""jje\x04""jjee\x03""jji\x03""dlu\x03""dlo\x03""dle\x04""dlee\x03""dli\x03""lhu\x03""lhe\x04""lhee\x04""tlhu\x05""tlhee\x04""tlee\x03""dzu\x03""dzo\x03""dze\x04""dzee\x03""dzi\x04""ttsu\x04""ttso\x04""ttse\x05""ttsee\x04""ttsi\x04""ttsa\x03""qai\x04""ngai\x04""nngi\x05""nngii\x04""nngo\x05""nngoo\x04""nnga\x05""nngaa\x03""sso\x02""ac\x03""ear\x03""ior\x08""boundary\x03""ang\x03""zra\x04""todo\x04""sibe\x06""manchu\x02""uk\x03""uuv\x02""ry\x03""ryy\x02""ly\x03""lyy\x02""ua\x02""kr\x03""yan\x09""mukphreng\x09""kemphreng\x04""sa-i\x02""eh\x03""aue\x05""tone-\x03""kva\x03""xva\x05""vowel\x03""aay\x02""uy\x03""oay\x03""uey\x02""iy\x05""final\x03""lae\x04""laev\x04""ngka\x03""mpa\x03""nra\x04""nyca\x03""ulu\x05""cecek\x06""surang\x05""bisah\x05""akara\x05""ikara\x05""ukara\x05""ekara\x06""aikara\x05""okara\x07""rerekan\x06""tedung\x04""suku\x06""taling\x05""pepet\x04""khot\x04""tzir\x02""ef\x03""zal\x06""asyura\x08""panyecek\x09""panglayar\x09""pangwisad\x09""pamingkal\x08""panyakra\x07""panyiku\x08""panghulu\x07""panyuku\x0a""panaelaeng\x08""panolong\x07""pamepet\x0a""paneuleung\x07""pamaaeh\x03""sya\x03""kla\x03""gla\x03""pla\x03""fla\x03""bla\x03""mla\x03""hla\x07""nyin-do\x04""kang\x03""ran\x02""at\x02""ag\x02""al\x03""aak\x03""aaj\x03""aam\x03""aaw\x02""is\x02""ih\x03""iny\x02""ir\x02""uc\x02""ud\x03""unn\x02""ep\x03""edd\x03""err\x03""ott\x02""ob\x02""ov\x02""oh\x07""capital\x04""open\x08""sideways\x03""top\x06""bottom\x06""voiced\x06""turned\x05""alpha\x05""schwa\x03""eng\x04""beta\x05""greek\x05""delta\x05""gamma\x03""rho\x08""cyrillic\x07""insular\x04""iota\x07""upsilon\x03""esh\x03""ezh\x03""eth\x08""reversed\x07""dotless\x06""script\x06""barred\x05""theta\x09""flattened\x02""av\x02""zr\x02""jy\x02""cy\x0c""middle-welsh\x07""epsilon\x03""eta\x07""omicron\x05""omega\x03"" ha\x03"" ga\x03"" zi\x04"" pai\x05"" yong\x05"" bing\x03""tie\x02""et\x03"" xi\x06"" zheng\x06"" chong\x05"" ping\x05"" shan\x06""shapes\x05"" xian\x04"" qia\x05"" jiao\x04"" jue\x04"" hui\x03"" li\x03"" mo\x04"" jin\x05"" zhuo\x04"" shu\x03"" ji\x03"" lu\x03"" le\x04"" you\x04"" sui\x04"" lan\x05"" peng\x03"" bi\x04"" nen\x04"" xia\x04"" zao\x03"" ti\x04"" jie\x04"" nao\x04"" shi\x04"" hua\x05"" lian\x05"" jian\x05"" beng\x06"" jiang\x05"" xing\x04"" bie\x04"" zai\x05"" chou\x04"" sou\x05"" niao\x04"" die\x06"" huang\x04"" dun\x03"" yi\x04"" tuo\x05"" jing\x04"" dai\x04"" cha\x04"" fen\x02""pp\x04"" wan\x04"" sao\x04"" xiu\x04"" gao\x04"" xue\x05"" weng\x03""ecu\x02""cl\x02""cr\x02""ff\x03""mil\x03""pts\x02""dr\x03"" fu\x04"" kou\x04"" chu\x04"" zhe\x03""iii\x02""iv\x03""vii\x04""viii\x02""ix\x03""xii\x05"" tian\x04"" suo\x04"" she\x06"" zhuan\x05"" tang\x06"" zhuai\x04"" yao\x03"" tu\x03"" mi\x05"" zhen\x04"" xie\x04"" lei\x04"" gai\x05"" juan\x05""above\x04"" qiu\x05"" ding\x04"" que\x03""and\x03"" ao\x04"" mei\x03"" ge\x04""with\x03"" qu\x04"" hou\x03""azu\x04""buky\x04""vede\x07""glagoli\x05""dobro\x05""yestu\x07""zhivete\x05""dzelo\x06""zemlja\x04""izhe\x07""initial\x06""djervi\x04""kako\x07""ljudije\x07""myslite\x05""nashi\x03""onu\x06""pokoji\x05""ritsi\x05""slovo\x06""tvrido\x03""uku\x05""fritu\x04""heru\x03""otu\x04""shta\x06""chrivi\x04""yeru\x04""yeri\x04""yati\x07""spidery\x03""yus\x07""iotated\x03""big\x04""fita\x07""izhitsa\x07""shtapic\x0a""trokutasti\x08""latinate\x08""tailless\x04""alfa\x04""vida\x05""dalda\x03""eie\x03""sou\x04""zata\x04""hate\x06""thethe\x05""iauda\x04""kapa\x05""laula\x03""ksi\x04""sima\x03""tau\x03""psi\x03""oou\x09""dialect-p\x03""old\x0d""cryptogrammic\x07""crossed\x08""akhmimic\x08""l-shaped\x03""yab\x04""yabh\x03""yag\x05""yaghh\x06""berber\x03""yaj\x03""yad\x04""yadh\x04""yadd\x05""yaddh\x03""yey\x03""yaf\x03""yak\x06""tuareg\x05""yakhh\x03""yah\x04""yahh\x04""yakh\x03""yaq\x04""yazh\x07""ahaggar\x03""yal\x03""yam\x03""yap\x04""yarr\x04""yagh\x04""ayer\x03""yas\x04""yass\x04""yash\x03""yat\x04""yath\x04""yach\x04""yatt\x03""yav\x03""yaw\x03""yay\x03""yaz\x0a""tawellemet\x04""yazz\x0d""labialization\x03""loa\x03""moa\x03""roa\x03""soa\x04""shoa\x03""boa\x03""toa\x03""coa\x03""noa\x04""nyoa\x03""zoa\x03""doa\x04""ddoa\x03""joa\x04""thoa\x04""choa\x04""phoa\x03""poa\x04""ggwa\x04""ggwi\x05""ggwee\x04""ggwe\x03""ssu\x03""ssi\x04""ssaa\x04""ssee\x03""sse\x03""cca\x03""ccu\x03""cci\x04""ccaa\x04""ccee\x03""cce\x03""cco\x03""zza\x03""zzu\x03""zzi\x04""zzaa\x04""zzee\x03""zze\x03""zzo\x04""ccha\x04""cchu\x04""cchi\x05""cchaa\x05""cchee\x04""cche\x04""ccho\x03""qya\x03""qyu\x03""qyi\x04""qyaa\x04""qyee\x03""qye\x03""qyo\x03""kya\x03""kyu\x03""kyi\x04""kyaa\x04""kyee\x03""kye\x03""kyo\x03""xya\x03""xyu\x03""xyi\x04""xyaa\x04""xyee\x03""xye\x03""xyo\x03""gya\x03""gyu\x03""gyi\x04""gyaa\x04""gyee\x03""gye\x03""gyo\x02""er\x02""es\x05""shcha\x05""es-te\x05""djerv\x09""monograph\x08""iotified\x06""little\x04""full\x08""surround\x08""overlaid\x02""gn\x06""kiyeok\x0b""ssangkiyeok\x0b""kiyeok-sios\x05""nieun\x0b""nieun-cieuc\x0b""nieun-hieuh\x06""tikeut\x0b""ssangtikeut\x05""rieul\x0c""rieul-kiyeok\x0b""rieul-mieum\x0b""rieul-pieup\x0a""rieul-sios\x0d""rieul-thieuth\x0d""rieul-phieuph\x0b""rieul-hieuh\x05""mieum\x05""pieup\x0a""ssangpieup\x0a""pieup-sios\x04""sios\x09""ssangsios\x05""ieung\x05""cieuc\x0a""ssangcieuc\x07""chieuch\x07""khieukh\x07""thieuth\x07""phieuph\x05""hieuh\x0a""ssangnieun\x10""rieul-pieup-sios\x0d""rieul-pansios\x11""rieul-yeorinhieuh\x0b""mieum-pieup\x0a""mieum-sios\x0d""mieum-pansios\x0d""kapyeounmieum\x0c""pieup-kiyeok\x0c""pieup-tikeut\x11""pieup-sios-kiyeok\x11""pieup-sios-tikeut\x0b""pieup-cieuc\x0d""pieup-thieuth\x0d""kapyeounpieup\x12""kapyeounssangpieup\x0b""sios-kiyeok\x0a""sios-nieun\x0b""sios-tikeut\x0a""sios-pieup\x0a""sios-cieuc\x07""pansios\x0a""ssangieung\x08""yesieung\x0d""yesieung-sios\x10""yesieung-pansios\x0f""kapyeounphieuph\x0a""ssanghieuh\x06""araeae\x03""enn\x03""onn\x03""ann\x03""inn\x02""im\x03""ngg\x04""ainn\x04""aunn\x03""ong\x04""innn\x05""ojeon\x06""chamko\x05""jueui\x04"" kua\x03"" wu\x04"" yin\x03"" si\x03"" ye\x04"" nuo\x03"" xu\x06"" xiong\x04"" liu\x04"" lin\x06"" xiang\x04"" xin\x04"" pan\x03"" ma\x05"" qian\x06"" zhong\x02"" n\x06"" cheng\x05"" fang\x04"" zuo\x05"" zhou\x05"" dong\x03"" su\x06"" jiong\x05"" wang\x04"" zhu\x05"" long\x05"" ying\x05"" miao\x03"" yu\x04"" luo\x05"" chai\x04"" hun\x04"" rao\x04"" han\x04"" tai\x03"" ai\x04"" jun\x02"" l\x05"" xiao\x05"" tiao\x04"" zha\x03"" ku\x03"" er\x05"" nang\x03"" qi\x04"" chi\x03"" mu\x03"" se\x06"" qiong\x03"" sa\x03"" pu\x03"" ta\x03"" ou\x05"" mian\x04"" wen\x05"" diao\x04"" mie\x05"" quan\x04"" cai\x06"" liang\x03"" gu\x04"" mao\x04"" gua\x04"" man\x05"" chui\x05"" huan\x05"" gong\x04"" nan\x05"" dian\x04"" yan\x03"" ci\x05"" lang\x03"" he\x04"" tou\x05"" pian\x02"" e\x04"" qie\x04"" rui\x05"" chan\x04"" dan\x04"" duo\x04"" fei\x05"" bang\x03"" ba\x05"" kuai\x05"" shen\x03"" pi\x05"" yang\x04"" bei\x04"" che\x05"" suan\x05"" heng\x04"" gui\x04"" lou\x04"" sun\x04"" zou\x04"" zhi\x04"" jia\x03"" hu\x03"" la\x03"" ke\x04"" wei\x05"" zhao\x04"" kui\x04"" fan\x06"" zhang\x05"" song\x04"" nei\x05"" chen\x04"" guo\x03"" ng\x03"" fa\x04"" hao\x04"" pou\x05"" hong\x04"" tun\x03"" bo\x04"" nie\x04"" wai\x05"" shou\x05"" ling\x04"" lun\x05"" chun\x04"" rou\x03"" ze\x06"" sheng\x04"" bai\x04"" gou\x03"" na\x03"" cu\x04"" kuo\x04"" lao\x04"" huo\x04"" sai\x05"" rong\x03"" ju\x04"" pao\x04"" can\x05"" nian\x05"" xuan\x04"" qin\x03"" bu\x05"" zang\x05"" mang\x04"" dui\x04"" bao\x06"" chang\x04"" gun\x05"" liao\x03"" da\x05"" meng\x05"" qiao\x05"" rang\x04"" yun\x04"" tao\x04"" lai\x04"" ban\x05"" chuo\x03"" nu\x04"" ran\x04"" sha\x04"" dou\x03"" po\x05"" tong\x06"" qiang\x04"" xun\x05"" pang\x04"" cao\x03"" an\x04"" mai\x04"" yue\x05"" huai\x04"" zan\x04"" hai\x05"" luan\x05"" ning\x03"" ya\x05"" ming\x04"" zui\x04"" cui\x03"" de\x05"" bian\x04"" nou\x04"" tui\x05"" zhan\x04"" cen\x04"" min\x03"" zu\x03"" ni\x04"" cuo\x04"" pei\x05"" gang\x05"" yuan\x05"" biao\x04"" dao\x04"" jiu\x04"" run\x03"" wo\x05"" cuan\x04"" ren\x04"" kai\x04"" men\x07"" chuang\x05"" feng\x05"" zhai\x03"" di\x04"" ben\x05"" zong\x05"" ceng\x05"" hang\x04"" nin\x05"" kong\x04"" lie\x06"" kuang\x04"" san\x03"" te\x05"" shun\x03"" ce\x04"" ang\x03"" ru\x07"" shuang\x05"" guai\x03"" wa\x05"" shai\x05"" tuan\x05"" piao\x04"" kun\x04"" qun\x06"" chuai\x05"" shao\x05"" duan\x04"" gen\x06"" guang\x04"" cou\x05"" nuan\x05"" reng\x04"" mou\x04"" nai\x05"" guan\x04"" hen\x06"" chuan\x05"" kuan\x05"" qing\x04"" pin\x05"" kang\x03"" du\x05"" neng\x04"" tan\x05"" cang\x05"" chao\x05"" nong\x04"" kan\x04"" ken\x05"" ting\x04"" gan\x04"" niu\x05"" ruan\x05"" cong\x05"" zeng\x05"" shui\x05"" geng\x05"" shuo\x05"" zuan\x05"" zhui\x03"" en\x05"" leng\x04"" cun\x03"" ne\x04"" bin\x04"" ruo\x04"" kao\x05"" dang\x05"" teng\x03"" ri\x05"" deng\x03"" za\x06"" niang\x03"" ca\x05"" sang\x05"" keng\x06"" shuai\x04"" pie\x04"" tie\x06"" shuan\x05"" chua\x04"" zen\x06"" shang\x03"" pa\x04"" fou\x04"" diu\x03"" fo\x03"" ka\x04"" lia\x04"" zun\x05"" seng\x05"" zhun\x06"" zhuen\x05"" shua\x02"" a\x04"" pen\x02"" m\x04"" gem\x03"" yo\x03"" re\x04"" dia\x04""inch\x06""gallon\x04""giga\x06""guinea\x08""kilogram\x08""kilowatt\x07""gramton\x06""koruna\x08""shilling\x05""dozen\x04""desi\x06""dollar\x07""percent\x08""building\x05""farad\x05""franc\x07""hectare\x04""peso\x07""pfennig\x05""point\x03""hon\x05""micro\x04""mile\x04""mark\x06""micron\x05""rupee\x05""ruble\x03""rem\x08""roentgen\x05""meiji\x02""gb\x03""cal\x02""pf\x09""microgram\x02""hz\x03""khz\x02""mm\x03""ms2\x03""kpa\x03""gpa\x05""rads2\x02""mv\x02""nw\x02""cc\x02""cd\x02""gy\x03""mol\x07"" zhuang\x04"" zei\x02"" t\x05"" zhua\x04"" sen\x04"" hei\x04"" hal\x06"" ppwun\x04"" nay\x04"" yai\x06"" sasou\x04"" kes\x05"" saai\x05"" haai\x03"" so\x07"" akutsu\x05"" gake\x05"" gomi\x04"" ama\x04"" sho\x04"" ten\x04"" gei\x03"" ki\x04"" lue\x04"" miu\x05"" moku\x06"" tochi\x06"" kasei\x07"" kunugi\x06"" hazou\x08"" katsura\x05"" tamo\x0a"" shitamizu\x07"" shibui\x05"" tani\x05"" suei\x05"" diou\x08"" oozutsu\x0d"" tsumekanmuri\x04"" swu\x0c"" deshiguramu\x0b"" miriguramu\x0b"" hekutogura\x07"" tatamu\x04"" nue\x07"" utsubo\x02"" o\x04"" sik\x07"" sasara\x05"" yana\x03""bup\x05"" hata\x03""pap\x04""purx\x05"" kuji\x08"" shinshi\x04""nbap\x05"" kume\x04""nbyx\x09"" nukamiso\x03"" ro\x04""hmyx\x05""hmyrx\x07"" sukumo\x06"" kouji\x05"" kinu\x05"" wata\x04"" sok\x05"" kase\x06"" yingl\x07"" kasuri\x05"" nawa\x07"" odoshi\x05"" horo\x04"" sem\x05"" jung\x03"" un\x04""zzyr\x08"" kaakeru\x04""ssyt\x04""zhux\x09"" yashinau\x03""jyt\x03""qie\x04""njup\x04""nyuo\x08"" shikato\x03""xie\x0a"" tsuraneru\x03""een\x04""ween\x04""bhee\x04""mbee\x04""kpee\x05""mgbee\x04""gbee\x04""dhee\x05""dhhee\x04""ndee\x04""njee\x05""nggee\x03""hin\x03""win\x03""bhi\x03""mbi\x03""kpi\x04""mgbi\x03""gbi\x03""dhi\x04""dhhi\x03""ndi\x03""nji\x04""nggi\x04""ngan\x03""han\x03""wan\x03""mba\x04""kpan\x04""mgba\x04""dhha\x03""nda\x03""nja\x04""ngga\x03""oon\x04""woon\x04""bhoo\x03""boo\x04""mboo\x04""kpoo\x05""mgboo\x04""gboo\x03""voo\x04""dhoo\x05""dhhoo\x03""doo\x04""ndoo\x03""zoo\x04""zhoo\x03""joo\x04""njoo\x05""nggoo\x03""goo\x04""nyoo\x03""hun\x03""wun\x03""bhu\x03""mbu\x03""kpu\x04""mgbu\x03""gbu\x03""dhu\x04""dhhu\x03""ndu\x03""nju\x04""nggu\x04""ngon\x03""won\x03""bho\x03""mbo\x03""kpo\x04""mgbo\x03""gbo\x04""gbon\x03""dho\x04""dhho\x03""ndo\x03""njo\x04""nggo\x04""ngen\x03""hen\x03""wen\x03""bhe\x03""mbe\x03""kpe\x04""kpen\x04""mgbe\x03""gbe\x04""gben\x03""dhe\x04""dhhe\x03""nde\x04""ngge\x05""nggen\x03""gen\x0a""lengthener\x05""ndole\x06""zemlya\x05""broad\x07""neutral\x06""closed\x07""blended\x04""soft\x09""monocular\x09""binocular\x06""double\x0b""multiocular\x03""dwe\x04""dzwe\x04""zhwe\x04""dzze\x04""tswe\x04""tsse\x04""tche\x07""chinese\x06""dotted\x09""left-stem\x05""lower\x08""inverted\x06""stress\x0d""egyptological\x04""heng\x02""tz\x08""tresillo\x09""cuatrillo\x06""broken\x03""rum\x02""vy\x0a""visigothic\x05""thorn\x04""vend\x03""con\x02""us\x03""dum\x03""lum\x03""mum\x03""num\x03""tum\x02""um\x0a""circumflex\x05""colon\x06""equals\x08""saltillo\x08""dvisvara\x07""hasanta\x03""jho\x04""ddho\x03""rro\x09""alternate\x09""voiceless\x09""aspirated\x05""haaru\x03""hta\x04""shya\x04""nyja\x02""ea\x04""ngue\x04""chha\x04""nhue\x03""nha\x04""nhja\x03""nue\x03""ppa\x03""mue\x0b"" obiyaakasu\x04"" noy\x05"" tara\x07"" yadoru\x07"" hesaki\x04""gyon\x05"" sori\x07"" yofune\x05"" susa\x06"" usagi\x04"" nuc\x0b"" kutabireru\x05"" yaji\x07"" sonoko\x04"" hie\x04""nyan\x05"" hagi\x04"" ebi\x09"" kamakiri\x03""dab\x0a"" kamishimo\x05"" yuki\x04"" ena\x06"" hitoe\x08"" chihaya\x07"" tasuki\x08"" yasashi\x03""ren\x03""roe\x07"" segare\x06"" nerau\x07"" utsuke\x03""rim\x09"" shitsuke\x07"" yagate\x07"" suberu\x04"" sip\x03"" ip\x07"" totemo\x04"" kep\x05"" sako\x07"" appare\x06"" otoko\x0b"" sakenomoto\x09"" ishiyumi\x07"" habaki\x06"" irori\x06"" ngaak\x08"" kasugai\x06"" pyeng\x04""byun\x07"" kazari\x05"" yari\x05"" yuru\x07"" phwung\x04""song\x05"" tomo\x07"" kohaze\x03"" on\x07"" oroshi\x05"" shuu\x04"" eri\x07"" namazu\x05"" todo\x07"" kajika\x03""yon\x05"" bora\x05"" mate\x05"" gori\x05"" ugui\x06"" asari\x0a"" subashiri\x09"" kazunoko\x07"" shachi\x06"" dojou\x08"" sukesou\x08"" muroaji\x07"" haraka\x02"" z\x09"" hatahata\x04"" eso\x05"" kyou\x07"" shiira\x06"" mutsu\x04"" nio\x05"" yiao\x06"" shigi\x08"" chidori\x05"" toki\x08"" ikaruga\x07"" kakesu\x06"" isuka\x0c"" kikuitadaki\x08"" tsugumi\x04""jjog\x04""jjon\x04""jjol\x04""jjom\x04""jjob\x04""jjos\x05""jjong\x04""jjoc\x04""jjwa\x05""jjwag\x05""jjwal\x06""jjwass\x05""jjwae\x02""it\x02""ip\x03""iet\x03""iex\x03""iep\x02""ax\x02""ap\x03""uox\x02""uo\x03""uop\x02""ox\x02""op\x02""ex\x03""bit\x03""bix\x03""bip\x04""biet\x04""biex\x03""bie\x04""biep\x03""bat\x03""bax\x03""bap\x04""buox\x03""buo\x04""buop\x03""bot\x03""box\x03""bop\x03""bex\x03""bep\x03""but\x03""bux\x04""burx\x03""bur\x03""byt\x03""byx\x02""by\x03""byp\x04""byrx\x03""byr\x03""pit\x03""pix\x03""pip\x04""piex\x03""pie\x04""piep\x03""pat\x03""pax\x04""puox\x03""puo\x04""puop\x03""pot\x03""pox\x03""pop\x03""put\x03""pux\x03""pup\x03""pur\x03""pyt\x03""pyx\x02""py\x03""pyp\x04""pyrx\x03""pyr\x04""bbit\x04""bbix\x03""bbi\x04""bbip\x05""bbiet\x05""bbiex\x04""bbie\x05""bbiep\x04""bbat\x04""bbax\x04""bbap\x05""bbuox\x04""bbuo\x05""bbuop\x04""bbot\x04""bbox\x03""bbo\x04""bbop\x04""bbex\x03""bbe\x04""bbep\x04""bbut\x04""bbux\x03""bbu\x04""bbup\x05""bburx\x04""bbur\x04""bbyt\x04""bbyx\x03""bby\x04""bbyp\x04""nbit\x04""nbix\x03""nbi\x04""nbip\x05""nbiex\x04""nbie\x05""nbiep\x04""nbat\x04""nbax\x03""nba\x04""nbot\x04""nbox\x03""nbo\x04""nbop\x04""nbut\x04""nbux\x03""nbu\x04""nbup\x05""nburx\x04""nbur\x04""nbyt\x03""nby\x04""nbyp\x05""nbyrx\x04""nbyr\x04""hmit\x04""hmix\x03""hmi\x04""hmip\x05""hmiex\x04""hmie\x05""hmiep\x04""hmat\x04""hmax\x03""hma\x04""hmap\x05""hmuox\x04""hmuo\x05""hmuop\x04""hmot\x04""hmox\x03""hmo\x04""hmop\x04""hmut\x04""hmux\x03""hmu\x04""hmup\x05""hmurx\x04""hmur\x03""hmy\x04""hmyp\x04""hmyr\x03""mit\x03""mix\x03""mip\x04""miex\x03""mie\x04""miep\x03""mat\x03""max\x03""map\x04""muot\x04""muox\x03""muo\x04""muop\x03""mot\x03""mox\x03""mop\x03""mex\x03""mut\x03""mux\x03""mup\x04""murx\x03""mur\x03""myt\x03""myx\x02""my\x03""myp\x03""fit\x03""fix\x03""fip\x03""fat\x03""fax\x03""fap\x03""fox\x03""fop\x03""fut\x03""fux\x03""fup\x04""furx\x03""fur\x03""fyt\x03""fyx\x02""fy\x03""fyp\x03""vit\x03""vix\x03""vip\x04""viet\x04""viex\x03""vie\x04""viep\x03""vat\x03""vax\x03""vap\x03""vot\x03""vox\x03""vop\x03""vex\x03""vep\x03""vut\x03""vux\x03""vup\x04""vurx\x03""vur\x03""vyt\x03""vyx\x03""vyp\x04""vyrx\x03""vyr\x03""dit\x03""dix\x03""dip\x04""diex\x03""die\x04""diep\x03""dat\x03""dax\x03""dap\x04""duox\x03""duo\x03""dox\x03""dop\x03""dex\x03""dep\x03""dut\x03""dux\x03""dup\x04""durx\x03""dur\x03""tit\x03""tix\x03""tip\x04""tiex\x04""tiep\x03""tat\x03""tax\x03""tap\x04""tuot\x04""tuox\x03""tuo\x04""tuop\x03""tot\x03""tox\x03""tex\x03""tep\x03""tut\x03""tux\x03""tup\x04""turx\x03""tur\x04""ddit\x04""ddix\x04""ddip\x05""ddiex\x04""ddie\x05""ddiep\x04""ddat\x04""ddax\x04""ddap\x05""dduox\x04""dduo\x05""dduop\x04""ddot\x04""ddox\x04""ddop\x04""ddex\x04""ddep\x04""ddut\x04""ddux\x04""ddup\x05""ddurx\x04""ddur\x04""ndit\x04""ndix\x04""ndip\x05""ndiex\x04""ndie\x04""ndat\x04""ndax\x04""ndap\x04""ndot\x04""ndox\x04""ndop\x04""ndex\x04""ndep\x04""ndut\x04""ndux\x04""ndup\x05""ndurx\x04""ndur\x04""hnit\x04""hnix\x03""hni\x04""hnip\x05""hniet\x05""hniex\x04""hnie\x05""hniep\x04""hnat\x04""hnax\x04""hnap\x05""hnuox\x04""hnuo\x04""hnot\x04""hnox\x04""hnop\x04""hnex\x03""hne\x04""hnep\x04""hnut\x03""nit\x03""nix\x03""nip\x04""niex\x03""nie\x04""niep\x03""nax\x03""nap\x04""nuox\x03""nuo\x04""nuop\x03""not\x03""nox\x03""nop\x03""nex\x03""nep\x03""nut\x03""nux\x03""nup\x04""nurx\x03""nur\x04""hlit\x04""hlix\x03""hli\x04""hlip\x05""hliex\x04""hlie\x05""hliep\x04""hlat\x04""hlax\x04""hlap\x05""hluox\x04""hluo\x05""hluop\x04""hlox\x03""hlo\x04""hlop\x04""hlex\x03""hle\x04""hlep\x04""hlut\x04""hlux\x03""hlu\x04""hlup\x05""hlurx\x04""hlur\x04""hlyt\x04""hlyx\x03""hly\x04""hlyp\x05""hlyrx\x04""hlyr\x03""lit\x03""lix\x03""lip\x04""liet\x04""liex\x03""lie\x04""liep\x03""lat\x03""lax\x03""lap\x04""luot\x04""luox\x03""luo\x04""luop\x03""lot\x03""lox\x03""lop\x03""lex\x03""lep\x03""lut\x03""lux\x03""lup\x04""lurx\x03""lur\x03""lyt\x03""lyx\x03""lyp\x04""lyrx\x03""lyr\x03""git\x03""gix\x03""gip\x04""giet\x04""giex\x03""gie\x04""giep\x03""gat\x03""gax\x03""gap\x04""guot\x04""guox\x03""guo\x04""guop\x03""got\x03""gox\x03""gop\x03""get\x03""gex\x03""gep\x03""gut\x03""gux\x03""gup\x04""gurx\x03""gur\x03""kit\x03""kix\x03""kip\x04""kiex\x03""kie\x04""kiep\x03""kat\x03""kax\x03""kap\x04""kuox\x03""kuo\x04""kuop\x03""kot\x03""kox\x03""kop\x03""ket\x03""kex\x03""kep\x03""kut\x03""kux\x03""kup\x04""kurx\x03""kur\x04""ggit\x04""ggix\x05""ggiex\x04""ggie\x05""ggiep\x04""ggat\x04""ggax\x04""ggap\x05""gguot\x05""gguox\x04""gguo\x05""gguop\x04""ggot\x04""ggox\x04""ggop\x04""gget\x04""ggex\x04""ggep\x04""ggut\x04""ggux\x04""ggup\x05""ggurx\x04""ggur\x05""mgiex\x04""mgie\x04""mgat\x04""mgax\x03""mga\x04""mgap\x05""mguox\x04""mguo\x05""mguop\x04""mgot\x04""mgox\x03""mgo\x04""mgop\x04""mgex\x03""mge\x04""mgep\x04""mgut\x04""mgux\x03""mgu\x04""mgup\x05""mgurx\x04""mgur\x04""hxit\x04""hxix\x03""hxi\x04""hxip\x05""hxiet\x05""hxiex\x04""hxie\x05""hxiep\x04""hxat\x04""hxax\x03""hxa\x04""hxap\x05""hxuot\x05""hxuox\x04""hxuo\x05""hxuop\x04""hxot\x04""hxox\x03""hxo\x04""hxop\x04""hxex\x03""hxe\x04""hxep\x05""ngiex\x04""ngie\x05""ngiep\x04""ngat\x04""ngax\x04""ngap\x05""nguot\x05""nguox\x04""nguo\x04""ngot\x04""ngox\x04""ngop\x04""ngex\x03""nge\x04""ngep\x03""hit\x04""hiex\x03""hat\x03""hax\x03""hap\x04""huot\x04""huox\x03""huo\x04""huop\x03""hot\x03""hox\x03""hop\x03""hex\x03""hep\x03""wat\x03""wax\x03""wap\x04""wuox\x03""wuo\x04""wuop\x03""wox\x03""wop\x03""wex\x03""wep\x03""zit\x03""zix\x03""zip\x04""ziex\x03""zie\x04""ziep\x03""zat\x03""zax\x03""zap\x04""zuox\x03""zuo\x04""zuop\x03""zot\x03""zox\x03""zop\x03""zex\x03""zep\x03""zut\x03""zux\x03""zup\x04""zurx\x03""zur\x03""zyt\x03""zyx\x02""zy\x03""zyp\x04""zyrx\x03""zyr\x03""cit\x03""cix\x03""cip\x04""ciet\x04""ciex\x03""cie\x04""ciep\x03""cat\x03""cax\x03""cap\x04""cuox\x03""cuo\x04""cuop\x03""cot\x03""cox\x03""cop\x03""cex\x03""cep\x03""cut\x03""cux\x03""cup\x04""curx\x03""cur\x03""cyt\x03""cyx\x03""cyp\x04""cyrx\x03""cyr\x04""zzit\x04""zzix\x04""zzip\x05""zziet\x05""zziex\x04""zzie\x05""zziep\x04""zzat\x04""zzax\x04""zzap\x04""zzox\x04""zzop\x04""zzex\x04""zzep\x04""zzux\x04""zzup\x05""zzurx\x04""zzur\x04""zzyt\x04""zzyx\x03""zzy\x04""zzyp\x05""zzyrx\x04""nzit\x04""nzix\x03""nzi\x04""nzip\x05""nziex\x04""nzie\x05""nziep\x04""nzat\x04""nzax\x03""nza\x04""nzap\x05""nzuox\x04""nzuo\x04""nzox\x04""nzop\x04""nzex\x03""nze\x04""nzux\x03""nzu\x04""nzup\x05""nzurx\x04""nzur\x04""nzyt\x04""nzyx\x03""nzy\x04""nzyp\x05""nzyrx\x04""nzyr\x03""sit\x03""six\x03""sip\x04""siex\x03""sie\x04""siep\x03""sat\x03""sax\x03""sap\x04""suox\x03""suo\x04""suop\x03""sot\x03""sox\x03""sop\x03""sex\x03""sep\x03""sut\x03""sux\x03""sup\x04""surx\x03""sur\x03""syt\x03""syx\x02""sy\x03""syp\x04""syrx\x03""syr\x04""ssit\x04""ssix\x04""ssip\x05""ssiex\x04""ssie\x05""ssiep\x04""ssat\x04""ssax\x04""ssap\x04""ssot\x04""ssox\x04""ssop\x04""ssex\x04""ssep\x04""ssut\x04""ssux\x04""ssup\x04""ssyx\x03""ssy\x04""ssyp\x05""ssyrx\x04""ssyr\x04""zhat\x04""zhax\x04""zhap\x05""zhuox\x04""zhuo\x05""zhuop\x04""zhot\x04""zhox\x04""zhop\x04""zhet\x04""zhex\x04""zhep\x04""zhut\x04""zhup\x05""zhurx\x04""zhur\x04""zhyt\x04""zhyx\x03""zhy\x04""zhyp\x05""zhyrx\x04""zhyr\x04""chat\x04""chax\x04""chap\x05""chuot\x05""chuox\x04""chuo\x05""chuop\x04""chot\x04""chox\x04""chop\x04""chet\x04""chex\x04""chep\x04""chux\x04""chup\x05""churx\x04""chur\x04""chyt\x04""chyx\x03""chy\x04""chyp\x05""chyrx\x04""chyr\x04""rrax\x05""rruox\x04""rruo\x04""rrot\x04""rrox\x04""rrop\x04""rret\x04""rrex\x03""rre\x04""rrep\x04""rrut\x04""rrux\x03""rru\x04""rrup\x05""rrurx\x04""rrur\x04""rryt\x04""rryx\x03""rry\x04""rryp\x05""rryrx\x04""rryr\x04""nrat\x04""nrax\x04""nrap\x04""nrox\x03""nro\x04""nrop\x04""nret\x04""nrex\x03""nre\x04""nrep\x04""nrut\x04""nrux\x03""nru\x04""nrup\x05""nrurx\x04""nrur\x04""nryt\x04""nryx\x03""nry\x04""nryp\x05""nryrx\x04""nryr\x04""shat\x04""shax\x04""shap\x05""shuox\x04""shuo\x05""shuop\x04""shot\x04""shox\x04""shop\x04""shet\x04""shex\x04""shep\x04""shut\x04""shux\x04""shup\x05""shurx\x04""shur\x04""shyt\x04""shyx\x03""shy\x04""shyp\x05""shyrx\x04""shyr\x03""rat\x03""rax\x03""rap\x04""ruox\x03""ruo\x04""ruop\x03""rot\x03""rox\x03""rop\x03""rex\x03""rep\x03""rut\x03""rux\x03""rup\x04""rurx\x03""rur\x03""ryt\x03""ryx\x03""ryp\x04""ryrx\x03""ryr\x03""jit\x03""jix\x03""jip\x04""jiet\x04""jiex\x03""jie\x04""jiep\x04""juot\x04""juox\x03""juo\x04""juop\x03""jot\x03""jox\x03""jop\x03""jut\x03""jux\x03""jup\x04""jurx\x03""jur\x03""jyx\x03""jyp\x04""jyrx\x03""jyr\x03""qit\x03""qix\x03""qip\x04""qiet\x04""qiex\x04""qiep\x04""quot\x04""quox\x04""quop\x03""qot\x03""qox\x03""qop\x03""qut\x03""qux\x03""qup\x04""qurx\x03""qur\x03""qyt\x03""qyx\x02""qy\x03""qyp\x04""qyrx\x03""qyr\x04""jjit\x04""jjix\x04""jjip\x05""jjiet\x05""jjiex\x04""jjie\x05""jjiep\x05""jjuox\x04""jjuo\x05""jjuop\x04""jjot\x04""jjox\x04""jjop\x04""jjut\x04""jjux\x04""jjup\x05""jjurx\x04""jjur\x04""jjyt\x04""jjyx\x03""jjy\x04""jjyp\x04""njit\x04""njix\x04""njip\x05""njiet\x05""njiex\x04""njie\x05""njiep\x05""njuox\x04""njuo\x04""njot\x04""njox\x04""njop\x04""njux\x05""njurx\x04""njur\x04""njyt\x04""njyx\x03""njy\x04""njyp\x05""njyrx\x04""njyr\x04""nyit\x04""nyix\x04""nyip\x05""nyiet\x05""nyiex\x04""nyie\x05""nyiep\x05""nyuox\x05""nyuop\x04""nyot\x04""nyox\x04""nyop\x04""nyut\x04""nyux\x04""nyup\x03""xit\x03""xix\x03""xip\x04""xiet\x04""xiex\x04""xiep\x04""xuox\x03""xuo\x03""xot\x03""xox\x03""xop\x03""xyt\x03""xyx\x02""xy\x03""xyp\x04""xyrx\x03""xyr\x03""yit\x03""yix\x03""yip\x04""yiet\x04""yiex\x03""yie\x04""yiep\x04""yuot\x04""yuox\x03""yuo\x04""yuop\x03""yot\x03""yox\x03""yop\x03""yut\x03""yux\x03""yup\x04""yurx\x03""yur\x03""yyt\x03""yyx\x03""yyp\x04""yyrx\x03""yyr\x03""kug\x03""kun\x03""kul\x03""kum\x03""kub\x03""kus\x04""kung\x04""kweo\x05""kweon\x05""kweol\x06""kweong\x05""kweng\x04""kwig\x04""kwin\x04""kwil\x04""kwim\x04""kwib\x04""kwis\x05""kwing\x04""kyun\x04""kyul\x04""kyum\x03""keu\x04""keug\x04""keun\x04""keul\x04""keum\x04""keub\x05""keung\x03""kig\x03""kin\x03""kil\x03""kim\x03""kib\x03""kis\x04""king\x03""tag\x03""tal\x04""talg\x03""tam\x03""tab\x03""tas\x04""tass\x04""tang\x03""tae\x04""taeg\x04""taen\x04""tael\x04""taem\x04""taeb\x04""taes\x05""taess\x05""taeng\x05""tyang\x03""teo\x04""teog\x04""teon\x04""teol\x05""teolm\x04""teom\x04""teob\x04""teos\x05""teoss\x05""teong\x03""teg\x03""ten\x03""tel\x03""tem\x03""teb\x03""tes\x04""teng\x04""tyeo\x05""tyeon\x06""tyeoss\x04""tyen\x03""tog\x03""ton\x03""tol\x03""tom\x03""tob\x03""tos\x04""tong\x04""twan\x04""twae\x03""toe\x04""toen\x04""toes\x05""toeng\x03""tug\x03""tun\x03""tul\x03""tub\x03""tus\x04""tung\x04""tweo\x06""tweoss\x04""twig\x04""twin\x04""twil\x04""twim\x04""twib\x05""twing\x03""tyu\x04""tyun\x04""tyul\x04""tyum\x05""tyung\x03""teu\x04""teug\x04""teun\x04""teud\x04""teul\x05""teulm\x04""teum\x04""teub\x04""teus\x04""tyin\x04""tyil\x04""tyim\x04""tyib\x03""tig\x03""tin\x03""til\x03""tim\x03""tib\x03""tis\x04""ting\x03""pag\x04""pagg\x03""pan\x03""pal\x04""palm\x03""pam\x03""pab\x03""pas\x04""pass\x04""pang\x03""pae\x04""paeg\x04""paen\x04""pael\x04""paem\x04""paeb\x04""paes\x05""paess\x05""paeng\x03""pya\x04""pyag\x03""peo\x04""peog\x04""peon\x04""peol\x04""peom\x04""peob\x04""peos\x05""peoss\x05""peong\x03""peg\x03""pen\x03""pel\x03""pem\x03""peb\x03""pes\x04""peng\x04""pyeo\x05""pyeon\x05""pyeol\x05""pyeom\x05""pyeob\x06""pyeoss\x06""pyeong\x03""pye\x04""pyel\x04""pyeb\x04""pyes\x03""pog\x03""pon\x03""pol\x03""pom\x03""pob\x03""pos\x04""pong\x05""pwang\x03""poe\x04""poen\x03""pyo\x04""pyon\x04""pyol\x04""pyob\x04""pyos\x03""pug\x03""pun\x03""pud\x03""pul\x04""pulm\x03""pum\x03""pub\x03""pus\x04""pung\x04""pweo\x06""pweong\x04""pwin\x04""pwil\x04""pwim\x04""pwis\x03""pyu\x04""pyun\x04""pyul\x04""pyum\x04""pyus\x05""pyung\x03""peu\x04""peun\x04""peul\x04""peum\x04""peub\x04""peus\x03""pig\x03""pin\x03""pil\x03""pim\x03""pib\x03""pis\x04""ping\x03""hag\x03""hal\x04""halt\x03""ham\x03""hab\x03""has\x04""hang\x04""haeg\x04""haen\x04""hael\x04""haem\x04""haeb\x04""haes\x05""haess\x05""haeng\x03""hya\x05""hyang\x03""heo\x04""heog\x04""heon\x04""heol\x05""heolm\x04""heom\x04""heob\x04""heos\x05""heong\x03""heg\x03""hel\x03""hem\x03""heb\x03""hes\x04""hyeo\x05""hyeog\x05""hyeon\x05""hyeol\x05""hyeom\x05""hyeob\x05""hyeos\x06""hyeoss\x06""hyeong\x03""hye\x04""hyen\x04""hyel\x04""hyeb\x03""hog\x03""hol\x04""holt\x03""hom\x03""hob\x03""hos\x04""hong\x04""hwag\x04""hwan\x04""hwal\x04""hwas\x05""hwang\x04""hwae\x05""hwaeg\x05""hwaen\x05""hwaes\x06""hwaeng\x04""hoeg\x04""hoen\x04""hoel\x04""hoeb\x04""hoes\x05""hoeng\x03""hyo\x04""hyon\x04""hyol\x04""hyob\x04""hyos\x03""hug\x03""hul\x04""hult\x03""hum\x03""hus\x04""hung\x04""hweo\x05""hweon\x05""hweol\x05""hweom\x06""hweong\x04""hweg\x03""gag\x04""gagg\x04""gags\x04""ganj\x04""ganh\x03""gad\x03""gal\x04""galg\x04""galm\x04""galb\x04""gals\x04""galt\x04""galp\x04""galh\x03""gam\x03""gab\x04""gabs\x03""gas\x04""gass\x04""gang\x03""gaj\x03""gac\x03""gak\x03""gah\x03""gae\x04""gaeg\x05""gaegg\x05""gaegs\x04""gaen\x05""gaenj\x05""gaenh\x04""gaed\x04""gael\x05""gaelg\x05""gaelm\x05""gaelb\x05""gaels\x05""gaelt\x05""gaelp\x05""gaelh\x04""gaem\x04""gaeb\x05""gaebs\x04""gaes\x05""gaess\x05""gaeng\x04""gaej\x04""gaec\x04""gaek\x04""gaet\x04""gaep\x04""gaeh\x04""gyag\x05""gyagg\x05""gyags\x04""gyan\x05""gyanj\x05""gyanh\x04""gyad\x04""gyal\x05""gyalg\x05""gyalm\x05""gyalb\x05""gyals\x05""gyalt\x05""gyalp\x05""gyalh\x04""gyam\x04""gyab\x05""gyabs\x04""gyas\x05""gyass\x05""gyang\x04""gyaj\x04""gyac\x04""gyak\x04""gyat\x04""gyap\x04""gyah\x04""gyae\x05""gyaeg\x06""gyaegg\x06""gyaegs\x05""gyaen\x06""gyaenj\x06""gyaenh\x05""gyaed\x05""gyael\x06""gyaelg\x06""gyaelm\x06""gyaelb\x06""gyaels\x06""gyaelt\x06""gyaelp\x06""gyaelh\x05""gyaem\x05""gyaeb\x06""gyaebs\x05""gyaes\x06""gyaess\x06""gyaeng\x05""gyaej\x05""gyaec\x05""gyaek\x05""gyaet\x05""gyaep\x05""gyaeh\x04""geog\x05""geogg\x05""geogs\x04""geon\x05""geonj\x05""geonh\x04""geod\x04""geol\x05""geolg\x05""geolm\x05""geolb\x05""geols\x05""geolt\x05""geolp\x05""geolh\x04""geom\x04""geob\x05""geobs\x04""geos\x05""geoss\x05""geong\x04""geoj\x04""geoc\x04""geok\x04""geot\x04""geop\x04""geoh\x03""geg\x04""gegg\x04""gegs\x04""genj\x04""genh\x03""ged\x03""gel\x04""gelg\x04""gelm\x04""gelb\x04""gels\x04""gelt\x04""gelp\x04""gelh\x03""gem\x03""geb\x04""gebs\x03""ges\x04""gess\x04""geng\x03""gej\x03""gec\x03""gek\x03""geh\x04""gyeo\x05""gyeog\x06""gyeogg\x06""gyeogs\x05""gyeon\x06""gyeonj\x06""gyeonh\x05""gyeod\x05""gyeol\x06""gyeolg\x06""gyeolm\x06""gyeolb\x06""gyeols\x06""gyeolt\x06""gyeolp\x06""gyeolh\x05""gyeom\x05""gyeob\x06""gyeobs\x05""gyeos\x06""gyeoss\x06""gyeong\x05""gyeoj\x05""gyeoc\x05""gyeok\x05""gyeot\x05""gyeop\x05""gyeoh\x04""gyeg\x05""gyegg\x05""gyegs\x04""gyen\x05""gyenj\x05""gyenh\x04""gyed\x04""gyel\x05""gyelg\x05""gyelm\x05""gyelb\x05""gyels\x05""gyelt\x05""gyelp\x05""gyelh\x04""gyem\x04""gyeb\x05""gyebs\x04""gyes\x05""gyess\x05""gyeng\x04""gyej\x04""gyec\x04""gyek\x04""gyet\x04""gyep\x04""gyeh\x03""gog\x04""gogg\x04""gogs\x03""gon\x04""gonj\x04""gonh\x03""god\x03""gol\x04""golg\x04""golm\x04""golb\x04""gols\x04""golt\x04""golp\x04""golh\x03""gom\x03""gob\x04""gobs\x03""gos\x04""goss\x04""gong\x03""goj\x03""goc\x03""gok\x03""goh\x04""gwag\x05""gwagg\x05""gwags\x04""gwan\x05""gwanj\x05""gwanh\x04""gwad\x04""gwal\x05""gwalg\x05""gwalm\x05""gwalb\x05""gwals\x05""gwalt\x05""gwalp\x05""gwalh\x04""gwam\x04""gwab\x05""gwabs\x04""gwas\x05""gwass\x05""gwang\x04""gwaj\x04""gwac\x04""gwak\x04""gwat\x04""gwap\x04""gwah\x04""gwae\x05""gwaeg\x06""gwaegg\x06""gwaegs\x05""gwaen\x06""gwaenj\x06""gwaenh\x05""gwaed\x05""gwael\x06""gwaelg\x06""gwaelm\x06""gwaelb\x06""gwaels\x06""gwaelt\x06""gwaelp\x06""gwaelh\x05""gwaem\x05""gwaeb\x06""gwaebs\x05""gwaes\x06""gwaess\x06""gwaeng\x05""gwaej\x05""gwaec\x05""gwaek\x05""gwaet\x05""gwaep\x05""gwaeh\x03""goe\x04""goeg\x05""goegg\x05""goegs\x04""goen\x05""goenj\x05""goenh\x04""goed\x04""goel\x05""goelg\x05""goelm\x05""goelb\x05""goels\x05""goelt\x05""goelp\x05""goelh\x04""goem\x04""goeb\x05""goebs\x04""goes\x05""goess\x05""goeng\x04""goej\x04""goec\x04""goek\x04""goet\x04""goep\x04""goeh\x04""gyog\x05""gyogg\x05""gyogs\x05""gyonj\x05""gyonh\x04""gyod\x04""gyol\x05""gyolg\x05""gyolm\x05""gyolb\x05""gyols\x05""gyolt\x05""gyolp\x05""gyolh\x04""gyom\x04""gyob\x05""gyobs\x04""gyos\x05""gyoss\x05""gyong\x04""gyoj\x04""gyoc\x04""gyok\x04""gyot\x04""gyop\x04""gyoh\x03""gug\x04""gugg\x04""gugs\x03""gun\x04""gunj\x04""gunh\x03""gud\x03""gul\x04""gulg\x04""gulm\x04""gulb\x04""guls\x04""gult\x04""gulp\x04""gulh\x03""gum\x03""gub\x04""gubs\x03""gus\x04""guss\x04""gung\x03""guj\x03""guc\x03""guk\x03""guh\x04""gweo\x05""gweog\x06""gweogg\x06""gweogs\x05""gweon\x06""gweonj\x06""gweonh\x05""gweod\x05""gweol\x06""gweolg\x06""gweolm\x06""gweolb\x06""gweols\x06""gweolt\x06""gweolp\x06""gweolh\x05""gweom\x05""gweob\x06""gweobs\x05""gweos\x06""gweoss\x06""gweong\x05""gweoj\x05""gweoc\x05""gweok\x05""gweot\x05""gweop\x05""gweoh\x04""gweg\x05""gwegg\x05""gwegs\x04""gwen\x05""gwenj\x05""gwenh\x04""gwed\x04""gwel\x05""gwelg\x05""gwelm\x05""gwelb\x05""gwels\x05""gwelt\x05""gwelp\x05""gwelh\x04""gwem\x04""gweb\x05""gwebs\x04""gwes\x05""gwess\x05""gweng\x04""gwej\x04""gwec\x04""gwek\x04""gwet\x04""gwep\x04""gweh\x04""gwig\x05""gwigg\x05""gwigs\x04""gwin\x05""gwinj\x05""gwinh\x04""gwid\x04""gwil\x05""gwilg\x05""gwilm\x05""gwilb\x05""gwils\x05""gwilt\x05""gwilp\x05""gwilh\x04""gwim\x04""gwib\x05""gwibs\x04""gwis\x05""gwiss\x05""gwing\x04""gwij\x04""gwic\x04""gwik\x04""gwit\x04""gwip\x04""gwih\x04""gyug\x05""gyugg\x05""gyugs\x04""gyun\x05""gyunj\x05""gyunh\x04""gyud\x04""gyul\x05""gyulg\x05""gyulm\x05""gyulb\x05""gyuls\x05""gyult\x05""gyulp\x05""gyulh\x04""gyum\x04""gyub\x05""gyubs\x04""gyus\x05""gyuss\x05""gyung\x04""gyuj\x04""gyuc\x04""gyuk\x04""gyut\x04""gyup\x04""gyuh\x03""geu\x04""geug\x05""geugg\x05""geugs\x04""geun\x05""geunj\x05""geunh\x04""geud\x04""geul\x05""geulg\x05""geulm\x05""geulb\x05""geuls\x05""geult\x05""geulp\x05""geulh\x04""geum\x04""geub\x05""geubs\x04""geus\x04""geuj\x04""geuc\x04""geuk\x04""geut\x04""geup\x04""geuh\x04""gyig\x05""gyigg\x05""gyigs\x04""gyin\x05""gyinj\x05""gyinh\x04""gyid\x04""gyil\x05""gyilg\x05""gyilm\x05""gyilb\x05""gyils\x05""gyilt\x05""gyilp\x05""gyilh\x04""gyim\x04""gyib\x05""gyibs\x04""gyis\x05""gyiss\x05""gying\x04""gyij\x04""gyic\x04""gyik\x04""gyit\x04""gyip\x04""gyih\x03""gig\x04""gigg\x04""gigs\x03""gin\x04""ginj\x04""ginh\x03""gid\x03""gil\x04""gilg\x04""gilm\x04""gilb\x04""gils\x04""gilt\x04""gilp\x04""gilh\x03""gim\x03""gib\x04""gibs\x03""gis\x04""giss\x04""ging\x03""gij\x03""gic\x03""gik\x03""gih\x04""ggag\x05""ggagg\x05""ggags\x04""ggan\x05""gganj\x05""gganh\x04""ggad\x04""ggal\x05""ggalg\x05""ggalm\x05""ggalb\x05""ggals\x05""ggalt\x05""ggalp\x05""ggalh\x04""ggam\x04""ggab\x05""ggabs\x04""ggas\x05""ggass\x05""ggang\x04""ggaj\x04""ggac\x04""ggak\x04""ggah\x04""ggae\x05""ggaeg\x06""ggaegg\x06""ggaegs\x05""ggaen\x06""ggaenj\x06""ggaenh\x05""ggaed\x05""ggael\x06""ggaelg\x06""ggaelm\x06""ggaelb\x06""ggaels\x06""ggaelt\x06""ggaelp\x06""ggaelh\x05""ggaem\x05""ggaeb\x06""ggaebs\x05""ggaes\x06""ggaess\x06""ggaeng\x05""ggaej\x05""ggaec\x05""ggaek\x05""ggaet\x05""ggaep\x05""ggaeh\x04""ggya\x05""ggyag\x06""ggyagg\x06""ggyags\x05""ggyan\x06""ggyanj\x06""ggyanh\x05""ggyad\x05""ggyal\x06""ggyalg\x06""ggyalm\x06""ggyalb\x06""ggyals\x06""ggyalt\x06""ggyalp\x06""ggyalh\x05""ggyam\x05""ggyab\x06""ggyabs\x05""ggyas\x06""ggyass\x06""ggyang\x05""ggyaj\x05""ggyac\x05""ggyak\x05""ggyat\x05""ggyap\x05""ggyah\x05""ggyae\x06""ggyaeg\x07""ggyaegg\x07""ggyaegs\x06""ggyaen\x07""ggyaenj\x07""ggyaenh\x06""ggyaed\x06""ggyael\x07""ggyaelg\x07""ggyaelm\x07""ggyaelb\x07""ggyaels\x07""ggyaelt\x07""ggyaelp\x07""ggyaelh\x06""ggyaem\x06""ggyaeb\x07""ggyaebs\x06""ggyaes\x07""ggyaess\x07""ggyaeng\x06""ggyaej\x06""ggyaec\x06""ggyaek\x06""ggyaet\x06""ggyaep\x06""ggyaeh\x04""ggeo\x05""ggeog\x06""ggeogg\x06""ggeogs\x05""ggeon\x06""ggeonj\x06""ggeonh\x05""ggeod\x05""ggeol\x06""ggeolg\x06""ggeolm\x06""ggeolb\x06""ggeols\x06""ggeolt\x06""ggeolp\x06""ggeolh\x05""ggeom\x05""ggeob\x06""ggeobs\x05""ggeos\x06""ggeoss\x06""ggeong\x05""ggeoj\x05""ggeoc\x05""ggeok\x05""ggeot\x05""ggeop\x05""ggeoh\x04""ggeg\x05""ggegg\x05""ggegs\x04""ggen\x05""ggenj\x05""ggenh\x04""gged\x04""ggel\x05""ggelg\x05""ggelm\x05""ggelb\x05""ggels\x05""ggelt\x05""ggelp\x05""ggelh\x04""ggem\x04""ggeb\x05""ggebs\x04""gges\x05""ggess\x05""ggeng\x04""ggej\x04""ggec\x04""ggek\x04""ggeh\x05""ggyeo\x06""ggyeog\x07""ggyeogg\x07""ggyeogs\x06""ggyeon\x07""ggyeonj\x07""ggyeonh\x06""ggyeod\x06""ggyeol\x07""ggyeolg\x07""ggyeolm\x07""ggyeolb\x07""ggyeols\x07""ggyeolt\x07""ggyeolp\x07""ggyeolh\x06""ggyeom\x06""ggyeob\x07""ggyeobs\x06""ggyeos\x07""ggyeoss\x07""ggyeong\x06""ggyeoj\x06""ggyeoc\x06""ggyeok\x06""ggyeot\x06""ggyeop\x06""ggyeoh\x04""ggye\x05""ggyeg\x06""ggyegg\x06""ggyegs\x05""ggyen\x06""ggyenj\x06""ggyenh\x05""ggyed\x05""ggyel\x06""ggyelg\x06""ggyelm\x06""ggyelb\x06""ggyels\x06""ggyelt\x06""ggyelp\x06""ggyelh\x05""ggyem\x05""ggyeb\x06""ggyebs\x05""ggyes\x06""ggyess\x06""ggyeng\x05""ggyej\x05""ggyec\x05""ggyek\x05""ggyet\x05""ggyep\x05""ggyeh\x04""ggog\x05""ggogg\x05""ggogs\x04""ggon\x05""ggonj\x05""ggonh\x04""ggod\x04""ggol\x05""ggolg\x05""ggolm\x05""ggolb\x05""ggols\x05""ggolt\x05""ggolp\x05""ggolh\x04""ggom\x04""ggob\x05""ggobs\x04""ggos\x05""ggoss\x05""ggong\x04""ggoj\x04""ggoc\x04""ggok\x04""ggoh\x05""ggwag\x06""ggwagg\x06""ggwags\x05""ggwan\x06""ggwanj\x06""ggwanh\x05""ggwad\x05""ggwal\x06""ggwalg\x06""ggwalm\x06""ggwalb\x06""ggwals\x06""ggwalt\x06""ggwalp\x06""ggwalh\x05""ggwam\x05""ggwab\x06""ggwabs\x05""ggwas\x06""ggwass\x06""ggwang\x05""ggwaj\x05""ggwac\x05""ggwak\x05""ggwat\x05""ggwap\x05""ggwah\x05""ggwae\x06""ggwaeg\x07""ggwaegg\x07""ggwaegs\x06""ggwaen\x07""ggwaenj\x07""ggwaenh\x06""ggwaed\x06""ggwael\x07""ggwaelg\x07""ggwaelm\x07""ggwaelb\x07""ggwaels\x07""ggwaelt\x07""ggwaelp\x07""ggwaelh\x06""ggwaem\x06""ggwaeb\x07""ggwaebs\x06""ggwaes\x07""ggwaess\x07""ggwaeng\x06""ggwaej\x06""ggwaec\x06""ggwaek\x06""ggwaet\x06""ggwaep\x06""ggwaeh\x04""ggoe\x05""ggoeg\x06""ggoegg\x06""ggoegs\x05""ggoen\x06""ggoenj\x06""ggoenh\x05""ggoed\x05""ggoel\x06""ggoelg\x06""ggoelm\x06""ggoelb\x06""ggoels\x06""ggoelt\x06""ggoelp\x06""ggoelh\x05""ggoem\x05""ggoeb\x06""ggoebs\x05""ggoes\x06""ggoess\x06""ggoeng\x05""ggoej\x05""ggoec\x05""ggoek\x05""ggoet\x05""ggoep\x05""ggoeh\x04""ggyo\x05""ggyog\x06""ggyogg\x06""ggyogs\x05""ggyon\x06""ggyonj\x06""ggyonh\x05""ggyod\x05""ggyol\x06""ggyolg\x06""ggyolm\x06""ggyolb\x06""ggyols\x06""ggyolt\x06""ggyolp\x06""ggyolh\x05""ggyom\x05""ggyob\x06""ggyobs\x05""ggyos\x06""ggyoss\x06""ggyong\x05""ggyoj\x05""ggyoc\x05""ggyok\x05""ggyot\x05""ggyop\x05""ggyoh\x04""ggug\x05""ggugg\x05""ggugs\x04""ggun\x05""ggunj\x05""ggunh\x04""ggud\x04""ggul\x05""ggulg\x05""ggulm\x05""ggulb\x05""gguls\x05""ggult\x05""ggulp\x05""ggulh\x04""ggum\x04""ggub\x05""ggubs\x04""ggus\x05""gguss\x05""ggung\x04""gguj\x04""gguc\x04""gguk\x04""gguh\x05""ggweo\x06""ggweog\x07""ggweogg\x07""ggweogs\x06""ggweon\x07""ggweonj\x07""ggweonh\x06""ggweod\x06""ggweol\x07""ggweolg\x07""ggweolm\x07""ggweolb\x07""ggweols\x07""ggweolt\x07""ggweolp\x07""ggweolh\x06""ggweom\x06""ggweob\x07""ggweobs\x06""ggweos\x07""ggweoss\x07""ggweong\x06""ggweoj\x06""ggweoc\x06""ggweok\x06""ggweot\x06""ggweop\x06""ggweoh\x05""ggweg\x06""ggwegg\x06""ggwegs\x05""ggwen\x06""ggwenj\x06""ggwenh\x05""ggwed\x05""ggwel\x06""ggwelg\x06""ggwelm\x06""ggwelb\x06""ggwels\x06""ggwelt\x06""ggwelp\x06""ggwelh\x05""ggwem\x05""ggweb\x06""ggwebs\x05""ggwes\x06""ggwess\x06""ggweng\x05""ggwej\x05""ggwec\x05""ggwek\x05""ggwet\x05""ggwep\x05""ggweh\x05""ggwig\x06""ggwigg\x06""ggwigs\x05""ggwin\x06""ggwinj\x06""ggwinh\x05""ggwid\x05""ggwil\x06""ggwilg\x06""ggwilm\x06""ggwilb\x06""ggwils\x06""ggwilt\x06""ggwilp\x06""ggwilh\x05""ggwim\x05""ggwib\x06""ggwibs\x05""ggwis\x06""ggwiss\x06""ggwing\x05""ggwij\x05""ggwic\x05""ggwik\x05""ggwit\x05""ggwip\x05""ggwih\x04""ggyu\x05""ggyug\x06""ggyugg\x06""ggyugs\x05""ggyun\x06""ggyunj\x06""ggyunh\x05""ggyud\x05""ggyul\x06""ggyulg\x06""ggyulm\x06""ggyulb\x06""ggyuls\x06""ggyult\x06""ggyulp\x06""ggyulh\x05""ggyum\x05""ggyub\x06""ggyubs\x05""ggyus\x06""ggyuss\x06""ggyung\x05""ggyuj\x05""ggyuc\x05""ggyuk\x05""ggyut\x05""ggyup\x05""ggyuh\x04""ggeu\x05""ggeug\x06""ggeugg\x06""ggeugs\x05""ggeun\x06""ggeunj\x06""ggeunh\x05""ggeud\x05""ggeul\x06""ggeulg\x06""ggeulm\x06""ggeulb\x06""ggeuls\x06""ggeult\x06""ggeulp\x06""ggeulh\x05""ggeum\x05""ggeub\x06""ggeubs\x05""ggeus\x06""ggeuss\x06""ggeung\x05""ggeuj\x05""ggeuc\x05""ggeuk\x05""ggeut\x05""ggeup\x05""ggeuh\x04""ggyi\x05""ggyig\x06""ggyigg\x06""ggyigs\x05""ggyin\x06""ggyinj\x06""ggyinh\x05""ggyid\x05""ggyil\x06""ggyilg\x06""ggyilm\x06""ggyilb\x06""ggyils\x06""ggyilt\x06""ggyilp\x06""ggyilh\x05""ggyim\x05""ggyib\x06""ggyibs\x05""ggyis\x06""ggyiss\x06""ggying\x05""ggyij\x05""ggyic\x05""ggyik\x05""ggyit\x05""ggyip\x05""ggyih\x04""ggig\x05""ggigg\x05""ggigs\x04""ggin\x05""gginj\x05""gginh\x04""ggid\x04""ggil\x05""ggilg\x05""ggilm\x05""ggilb\x05""ggils\x05""ggilt\x05""ggilp\x05""ggilh\x04""ggim\x04""ggib\x05""ggibs\x04""ggis\x05""ggiss\x05""gging\x04""ggij\x04""ggic\x04""ggik\x04""ggip\x04""ggih\x03""nag\x04""nagg\x04""nags\x03""nan\x04""nanj\x04""nanh\x03""nad\x03""nal\x04""nalg\x04""nalm\x04""nalb\x04""nals\x04""nalt\x04""nalp\x04""nalh\x03""nam\x03""nab\x04""nabs\x03""nas\x04""nass\x04""nang\x03""naj\x03""nac\x03""nak\x03""nat\x03""nae\x04""naeg\x05""naegg\x05""naegs\x04""naen\x05""naenj\x05""naenh\x04""naed\x04""nael\x05""naelg\x05""naelm\x05""naelb\x05""naels\x05""naelt\x05""naelp\x05""naelh\x04""naem\x04""naeb\x05""naebs\x04""naes\x05""naess\x05""naeng\x04""naej\x04""naec\x04""naek\x04""naet\x04""naep\x04""naeh\x04""nyag\x05""nyagg\x05""nyags\x05""nyanj\x05""nyanh\x04""nyad\x04""nyal\x05""nyalg\x05""nyalm\x05""nyalb\x05""nyals\x05""nyalt\x05""nyalp\x05""nyalh\x04""nyam\x04""nyab\x05""nyabs\x04""nyas\x05""nyass\x05""nyang\x04""nyaj\x04""nyac\x04""nyak\x04""nyat\x04""nyap\x04""nyah\x04""nyae\x05""nyaeg\x06""nyaegg\x06""nyaegs\x05""nyaen\x06""nyaenj\x06""nyaenh\x05""nyaed\x05""nyael\x06""nyaelg\x06""nyaelm\x06""nyaelb\x06""nyaels\x06""nyaelt\x06""nyaelp\x06""nyaelh\x05""nyaem\x05""nyaeb\x06""nyaebs\x05""nyaes\x06""nyaess\x06""nyaeng\x05""nyaej\x05""nyaec\x05""nyaek\x05""nyaet\x05""nyaep\x05""nyaeh\x03""neo\x04""neog\x05""neogg\x05""neogs\x04""neon\x05""neonj\x05""neonh\x04""neod\x04""neol\x05""neolg\x05""neolm\x05""neolb\x05""neols\x05""neolt\x05""neolp\x05""neolh\x04""neom\x04""neob\x05""neobs\x04""neos\x05""neoss\x05""neong\x04""neoj\x04""neoc\x04""neok\x04""neot\x04""neop\x04""neoh\x03""neg\x04""negg\x04""negs\x03""nen\x04""nenj\x04""nenh\x03""ned\x03""nel\x04""nelg\x04""nelm\x04""nelb\x04""nels\x04""nelt\x04""nelp\x04""nelh\x03""nem\x03""neb\x04""nebs\x03""nes\x04""ness\x04""neng\x03""nej\x03""nec\x03""nek\x03""net\x03""neh\x04""nyeo\x05""nyeog\x06""nyeogg\x06""nyeogs\x05""nyeon\x06""nyeonj\x06""nyeonh\x05""nyeod\x05""nyeol\x06""nyeolg\x06""nyeolm\x06""nyeolb\x06""nyeols\x06""nyeolt\x06""nyeolp\x06""nyeolh\x05""nyeom\x05""nyeob\x06""nyeobs\x05""nyeos\x06""nyeoss\x06""nyeong\x05""nyeoj\x05""nyeoc\x05""nyeok\x05""nyeot\x05""nyeop\x05""nyeoh\x04""nyeg\x05""nyegg\x05""nyegs\x04""nyen\x05""nyenj\x05""nyenh\x04""nyed\x04""nyel\x05""nyelg\x05""nyelm\x05""nyelb\x05""nyels\x05""nyelt\x05""nyelp\x05""nyelh\x04""nyem\x04""nyeb\x05""nyebs\x04""nyes\x05""nyess\x05""nyeng\x04""nyej\x04""nyec\x04""nyek\x04""nyet\x04""nyep\x04""nyeh\x03""nog\x04""nogg\x04""nogs\x03""non\x04""nonj\x04""nonh\x03""nod\x03""nol\x04""nolg\x04""nolm\x04""nolb\x04""nols\x04""nolt\x04""nolp\x04""nolh\x03""nom\x03""nob\x04""nobs\x03""nos\x04""noss\x04""nong\x03""noj\x03""noc\x03""nok\x03""noh\x04""nwag\x05""nwagg\x05""nwags\x04""nwan\x05""nwanj\x05""nwanh\x04""nwad\x04""nwal\x05""nwalg\x05""nwalm\x05""nwalb\x05""nwals\x05""nwalt\x05""nwalp\x05""nwalh\x04""nwam\x04""nwab\x05""nwabs\x04""nwas\x05""nwass\x05""nwang\x04""nwaj\x04""nwac\x04""nwak\x04""nwat\x04""nwap\x04""nwah\x04""nwae\x05""nwaeg\x06""nwaegg\x06""nwaegs\x05""nwaen\x06""nwaenj\x06""nwaenh\x05""nwaed\x05""nwael\x06""nwaelg\x06""nwaelm\x06""nwaelb\x06""nwaels\x06""nwaelt\x06""nwaelp\x06""nwaelh\x05""nwaem\x05""nwaeb\x06""nwaebs\x05""nwaes\x06""nwaess\x06""nwaeng\x05""nwaej\x05""nwaec\x05""nwaek\x05""nwaet\x05""nwaep\x05""nwaeh\x03""noe\x04""noeg\x05""noegg\x05""noegs\x04""noen\x05""noenj\x05""noenh\x04""noed\x04""noel\x05""noelg\x05""noelm\x05""noelb\x05""noels\x05""noelt\x05""noelp\x05""noelh\x04""noem\x04""noeb\x05""noebs\x04""noes\x05""noess\x05""noeng\x04""noej\x04""noec\x04""noek\x04""noet\x04""noep\x04""noeh\x04""nyog\x05""nyogg\x05""nyogs\x04""nyon\x05""nyonj\x05""nyonh\x04""nyod\x04""nyol\x05""nyolg\x05""nyolm\x05""nyolb\x05""nyols\x05""nyolt\x05""nyolp\x05""nyolh\x04""nyom\x04""nyob\x05""nyobs\x04""nyos\x05""nyoss\x05""nyong\x04""nyoj\x04""nyoc\x04""nyok\x04""nyoh\x03""nug\x04""nugg\x04""nugs\x03""nun\x04""nunj\x04""nunh\x03""nud\x03""nul\x04""nulg\x04""nulm\x04""nulb\x04""nuls\x04""nult\x04""nulp\x04""nulh\x03""nub\x04""nubs\x03""nus\x04""nuss\x04""nung\x03""nuj\x03""nuc\x03""nuk\x03""nuh\x04""nweo\x05""nweog\x06""nweogg\x06""nweogs\x05""nweon\x06""nweonj\x06""nweonh\x05""nweod\x05""nweol\x06""nweolg\x06""nweolm\x06""nweolb\x06""nweols\x06""nweolt\x06""nweolp\x06""nweolh\x05""nweom\x05""nweob\x06""nweobs\x05""nweos\x06""nweoss\x06""nweong\x05""nweoj\x05""nweoc\x05""nweok\x05""nweot\x05""nweop\x05""nweoh\x04""nweg\x05""nwegg\x05""nwegs\x04""nwen\x05""nwenj\x05""nwenh\x04""nwed\x04""nwel\x05""nwelg\x05""nwelm\x05""nwelb\x05""nwels\x05""nwelt\x05""nwelp\x05""nwelh\x04""nwem\x04""nweb\x05""nwebs\x04""nwes\x05""nwess\x05""nweng\x04""nwej\x04""nwec\x04""nwek\x04""nwet\x04""nwep\x04""nweh\x03""nwi\x04""nwig\x05""nwigg\x05""nwigs\x04""nwin\x05""nwinj\x05""nwinh\x04""nwid\x04""nwil\x05""nwilg\x05""nwilm\x05""nwilb\x05""nwils\x05""nwilt\x05""nwilp\x05""nwilh\x04""nwim\x04""nwib\x05""nwibs\x04""nwis\x05""nwiss\x05""nwing\x04""nwij\x04""nwic\x04""nwik\x04""nwit\x04""nwip\x04""nwih\x04""nyug\x05""nyugg\x05""nyugs\x04""nyun\x05""nyunj\x05""nyunh\x04""nyud\x04""nyul\x05""nyulg\x05""nyulm\x05""nyulb\x05""nyuls\x05""nyult\x05""nyulp\x05""nyulh\x04""nyum\x04""nyub\x05""nyubs\x04""nyus\x05""nyuss\x05""nyung\x04""nyuj\x04""nyuc\x04""nyuk\x04""nyuh\x03""neu\x04""neug\x05""neugg\x05""neugs\x04""neun\x05""neunj\x05""neunh\x04""neud\x04""neul\x05""neulg\x05""neulm\x05""neulb\x05""neuls\x05""neult\x05""neulp\x05""neulh\x04""neum\x04""neub\x05""neubs\x04""neus\x05""neuss\x05""neung\x04""neuj\x04""neuc\x04""neuk\x04""neut\x04""neup\x04""neuh\x04""nyig\x05""nyigg\x05""nyigs\x04""nyin\x05""nyinj\x05""nyinh\x04""nyid\x04""nyil\x05""nyilg\x05""nyilm\x05""nyilb\x05""nyils\x05""nyilt\x05""nyilp\x05""nyilh\x04""nyim\x04""nyib\x05""nyibs\x04""nyis\x05""nyiss\x05""nying\x04""nyij\x04""nyic\x04""nyik\x04""nyih\x03""nig\x04""nigg\x04""nigs\x03""nin\x04""ninj\x04""ninh\x03""nid\x03""nil\x04""nilg\x04""nilm\x04""nilb\x04""nils\x04""nilt\x04""nilp\x04""nilh\x03""nim\x03""nib\x04""nibs\x03""nis\x04""niss\x04""ning\x03""nij\x03""nic\x03""nik\x03""nih\x03""dag\x04""dagg\x04""dags\x03""dan\x04""danj\x04""danh\x03""dad\x04""dalg\x04""dalm\x04""dalb\x04""dals\x04""dalt\x04""dalp\x04""dalh\x03""dam\x04""dabs\x03""das\x04""dass\x04""dang\x03""daj\x03""dac\x03""dak\x03""dah\x03""dae\x04""daeg\x05""daegg\x05""daegs\x04""daen\x05""daenj\x05""daenh\x04""daed\x04""dael\x05""daelg\x05""daelm\x05""daelb\x05""daels\x05""daelt\x05""daelp\x05""daelh\x04""daem\x04""daeb\x05""daebs\x04""daes\x05""daess\x05""daeng\x04""daej\x04""daec\x04""daek\x04""daet\x04""daep\x04""daeh\x03""dya\x04""dyag\x05""dyagg\x05""dyags\x04""dyan\x05""dyanj\x05""dyanh\x04""dyad\x04""dyal\x05""dyalg\x05""dyalm\x05""dyalb\x05""dyals\x05""dyalt\x05""dyalp\x05""dyalh\x04""dyam\x04""dyab\x05""dyabs\x04""dyas\x05""dyass\x05""dyang\x04""dyaj\x04""dyac\x04""dyak\x04""dyat\x04""dyap\x04""dyah\x04""dyae\x05""dyaeg\x06""dyaegg\x06""dyaegs\x05""dyaen\x06""dyaenj\x06""dyaenh\x05""dyaed\x05""dyael\x06""dyaelg\x06""dyaelm\x06""dyaelb\x06""dyaels\x06""dyaelt\x06""dyaelp\x06""dyaelh\x05""dyaem\x05""dyaeb\x06""dyaebs\x05""dyaes\x06""dyaess\x06""dyaeng\x05""dyaej\x05""dyaec\x05""dyaek\x05""dyaet\x05""dyaep\x05""dyaeh\x03""deo\x04""deog\x05""deogg\x05""deogs\x04""deon\x05""deonj\x05""deonh\x04""deod\x04""deol\x05""deolg\x05""deolm\x05""deolb\x05""deols\x05""deolt\x05""deolp\x05""deolh\x04""deom\x04""deob\x05""deobs\x04""deos\x05""deoss\x05""deong\x04""deoj\x04""deoc\x04""deok\x04""deot\x04""deop\x04""deoh\x04""degg\x04""degs\x03""den\x04""denj\x04""denh\x03""ded\x03""del\x04""delg\x04""delm\x04""delb\x04""dels\x04""delt\x04""delp\x04""delh\x03""dem\x03""deb\x04""debs\x03""des\x04""dess\x04""deng\x03""dej\x03""dec\x03""dek\x03""det\x03""deh\x04""dyeo\x05""dyeog\x06""dyeogg\x06""dyeogs\x05""dyeon\x06""dyeonj\x06""dyeonh\x05""dyeod\x05""dyeol\x06""dyeolg\x06""dyeolm\x06""dyeolb\x06""dyeols\x06""dyeolt\x06""dyeolp\x06""dyeolh\x05""dyeom\x05""dyeob\x06""dyeobs\x05""dyeos\x06""dyeoss\x06""dyeong\x05""dyeoj\x05""dyeoc\x05""dyeok\x05""dyeot\x05""dyeop\x05""dyeoh\x03""dye\x04""dyeg\x05""dyegg\x05""dyegs\x04""dyen\x05""dyenj\x05""dyenh\x04""dyed\x04""dyel\x05""dyelg\x05""dyelm\x05""dyelb\x05""dyels\x05""dyelt\x05""dyelp\x05""dyelh\x04""dyem\x04""dyeb\x05""dyebs\x04""dyes\x05""dyess\x05""dyeng\x04""dyej\x04""dyec\x04""dyek\x04""dyet\x04""dyep\x04""dyeh\x03""dog\x04""dogg\x04""dogs\x04""donj\x04""donh\x03""dod\x03""dol\x04""dolg\x04""dolm\x04""dolb\x04""dols\x04""dolt\x04""dolp\x04""dolh\x03""dom\x03""dob\x04""dobs\x03""dos\x04""doss\x04""dong\x03""doj\x03""doc\x03""dok\x03""doh\x04""dwag\x05""dwagg\x05""dwags\x04""dwan\x05""dwanj\x05""dwanh\x04""dwad\x04""dwal\x05""dwalg\x05""dwalm\x05""dwalb\x05""dwals\x05""dwalt\x05""dwalp\x05""dwalh\x04""dwam\x04""dwab\x05""dwabs\x04""dwas\x05""dwass\x05""dwang\x04""dwaj\x04""dwac\x04""dwak\x04""dwat\x04""dwap\x04""dwah\x04""dwae\x05""dwaeg\x06""dwaegg\x06""dwaegs" -#define UTFASCIILOOKUP {1,1,1,1,1,1,1,1,1,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,2,1,2,2,1,1,2,2,2,2,4,6,8,10,12,14,16,18,20,22,2,2,1,1,1,1,1,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,1,2,1,1,1,1,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,28,76,1,72,1,79,1,28,24,1,1,2,58,2,82,86,8,10,1,64,54,1,2,6,52,1,88,91,94,1,24,24,24,24,24,24,97,28,32,32,32,32,40,40,40,40,30,50,52,52,52,52,52,70,52,64,64,64,64,64,100,79,24,24,24,24,24,24,97,28,32,32,32,32,40,40,40,40,30,50,52,52,52,52,52,2,52,64,64,64,64,72,100,72,24,24,24,24,24,24,28,28,28,28,28,28,28,28,30,30,30,30,32,32,32,32,32,32,32,32,32,32,36,36,36,36,36,36,36,36,38,38,38,38,40,40,40,40,40,40,40,40,40,40,103,103,42,42,44,44,44,46,46,46,46,46,46,46,46,46,46,50,50,50,50,50,50,50,106,106,52,52,52,52,52,52,109,109,58,58,58,58,58,58,60,60,60,60,60,60,60,60,62,62,62,62,62,62,64,64,64,64,64,64,64,64,64,64,64,64,68,68,72,72,72,74,74,74,74,74,74,60,26,26,26,26,16,16,52,28,28,30,30,30,30,30,10,1,32,34,34,36,36,112,40,40,44,44,46,46,68,50,50,52,52,52,115,115,54,54,118,8,8,121,121,62,62,62,62,64,64,72,66,72,72,74,74,124,124,124,124,8,14,14,127,68,1,1,1,1,130,130,130,133,133,133,136,136,136,24,24,40,40,52,52,64,64,64,64,64,64,64,64,64,64,1,24,24,24,24,97,97,36,36,36,36,44,44,52,52,52,52,124,124,42,130,30,130,36,36,112,68,50,50,24,24,97,97,52,52,24,24,24,24,32,32,32,32,40,40,40,40,52,52,52,52,58,58,58,58,64,64,64,64,60,60,62,62,72,72,38,38,50,30,139,139,74,74,24,24,32,32,52,52,52,52,52,52,52,52,72,72,46,50,62,42,142,145,24,28,28,46,62,60,74,148,148,26,64,66,32,32,42,42,56,56,58,58,72,72,24,24,24,26,52,28,30,30,32,1,1,32,32,32,32,42,36,36,36,36,64,72,38,38,40,40,40,46,46,46,153,68,68,48,50,50,50,52,109,52,34,58,58,58,58,58,58,58,58,58,60,60,42,60,60,62,62,64,64,66,1,68,72,72,74,74,74,74,1,1,1,28,1,26,32,36,38,42,44,46,56,1,1,130,130,130,127,127,156,159,162,153,165,1,38,38,44,38,42,58,58,58,58,68,72,1,1,1,1,1,1,1,1,1,1,1,1,66,1,66,1,2,2,2,2,1,2,2,2,2,1,1,1,66,1,2,66,2,1,2,1,1,58,70,36,46,60,70,1,168,179,184,188,193,203,207,66,1,1,212,217,220,225,231,236,236,236,243,249,256,262,256,262,266,266,220,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,272,272,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,272,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,24,32,40,52,64,28,30,38,48,58,62,66,70,278,278,283,283,1,2,289,289,0,0,1,0,0,0,1,0,0,0,0,0,1,1,24,2,32,32,40,0,52,0,64,52,40,24,26,36,30,32,74,32,100,40,44,46,48,50,300,52,54,58,0,60,62,64,303,306,76,52,40,64,24,32,32,40,64,24,26,36,30,32,74,32,100,40,44,46,48,50,70,52,54,58,60,60,62,64,303,306,76,52,40,64,52,64,52,0,26,100,64,64,64,303,54,2,309,309,315,315,68,68,56,56,318,318,121,121,34,34,306,306,38,38,36,36,321,321,324,324,44,58,28,42,0,0,0,327,327,0,331,331,0,0,0,0,335,338,341,344,335,130,40,347,42,133,136,350,354,40,64,357,24,26,66,36,30,335,124,74,40,40,44,46,48,50,52,54,58,60,62,64,34,306,127,321,121,361,366,72,1,32,371,374,24,26,66,36,30,335,124,74,40,40,44,46,48,50,52,54,58,60,62,64,34,306,127,321,121,361,366,72,1,32,371,374,335,338,341,344,335,130,40,347,42,133,136,350,354,40,64,357,52,52,32,32,335,335,32,32,335,335,52,52,338,338,300,300,76,76,34,34,72,72,72,72,64,64,52,52,52,52,380,380,56,56,383,1,1,1,1,0,388,395,40,40,1,1,58,58,36,36,36,36,36,36,124,124,74,74,44,44,44,44,44,44,44,44,50,50,106,106,54,54,306,306,60,60,62,62,64,64,64,64,306,306,403,403,321,321,321,321,38,38,321,321,321,321,1,124,124,44,44,407,407,50,50,410,410,321,321,413,413,416,24,24,24,24,97,97,335,335,1,1,1,1,124,124,74,74,130,130,40,40,40,40,52,52,52,52,52,52,32,32,64,64,64,64,64,64,321,321,425,425,72,72,425,425,429,429,429,429,432,432,435,435,439,439,443,443,448,448,452,452,456,456,460,460,464,464,407,407,467,467,471,471,475,475,479,479,482,482,485,485,407,407,410,410,0,0,0,0,0,0,0,0,0,0,0,0,0,24,26,36,30,32,74,32,32,62,124,40,46,306,127,44,38,130,377,321,48,72,50,121,52,321,54,42,491,60,66,62,58,127,68,54,44,52,34,0,0,220,0,0,0,0,0,0,0,24,26,36,30,32,74,32,32,62,124,40,46,306,127,44,38,130,377,321,48,72,50,121,52,321,54,42,491,60,66,62,58,127,68,54,44,52,34,494,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,24,52,40,32,32,24,24,52,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,497,26,36,30,38,66,74,306,62,72,44,44,46,48,48,50,50,60,502,54,54,127,127,56,58,121,62,0,0,0,0,0,66,507,40,0,0,0,0,0,0,0,0,0,0,0,0,0,0,510,0,516,0,0,0,0,0,0,0,0,0,0,0,522,528,540,547,559,564,0,497,0,0,0,0,0,0,0,0,0,0,574,24,497,68,497,72,497,26,580,62,100,42,38,306,30,584,58,74,60,121,60,30,62,74,587,36,591,591,597,597,597,0,34,56,44,46,48,50,38,68,497,72,603,606,609,24,64,40,68,0,0,0,0,0,0,0,0,66,612,618,0,0,0,0,6,8,10,12,14,16,18,20,22,0,0,0,0,622,626,497,497,497,497,179,179,179,64,179,630,633,26,62,62,54,100,637,640,38,644,647,38,321,650,654,30,30,657,584,660,30,30,30,664,58,58,58,58,58,58,42,58,60,60,60,60,60,62,377,34,34,34,66,34,303,56,56,306,44,44,44,106,44,36,36,50,36,36,36,46,46,46,46,50,50,50,50,50,38,321,667,38,38,580,68,109,109,64,670,670,68,66,72,72,72,68,32,673,72,72,0,97,677,626,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,681,685,0,6,8,10,12,14,16,18,20,22,121,30,377,689,689,696,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,700,700,26,36,36,30,30,38,68,74,38,62,62,72,706,44,46,48,50,60,60,32,54,54,60,56,58,121,62,709,709,709,24,24,24,24,24,24,32,32,32,32,40,40,64,64,64,52,0,0,0,0,0,70,56,0,0,0,0,0,0,717,717,717,622,622,622,622,622,622,622,640,640,681,681,685,725,587,587,587,730,730,591,591,591,734,734,739,739,739,744,685,685,725,640,640,725,685,640,497,497,597,597,597,748,748,673,673,640,725,725,752,38,121,50,58,26,46,44,756,66,48,34,584,100,46,36,644,60,30,74,62,72,54,42,321,630,762,306,100,74,121,60,30,62,74,765,377,56,68,24,770,40,773,64,776,32,779,52,782,0,785,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,8,10,12,14,16,18,20,22,24,773,40,32,64,776,52,789,50,800,803,806,809,812,816,819,822,826,829,833,836,839,842,845,848,842,429,852,855,848,858,858,858,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,863,875,884,24,24,770,40,892,64,895,58,46,898,32,32,905,898,52,52,908,836,911,915,918,922,926,812,809,929,848,933,937,942,946,951,806,955,816,959,842,963,803,968,800,972,845,855,819,822,839,976,980,985,988,992,826,429,0,0,996,1002,770,40,892,64,895,58,664,898,32,32,905,898,52,52,908,1011,0,0,0,1018,1025,0,0,0,0,0,479,1034,1039,1044,1047,471,833,1053,664,1057,46,1057,0,0,0,6,8,10,12,14,16,18,20,22,0,179,898,0,0,0,0,0,0,0,0,1060,1064,148,1068,1073,0,863,875,884,0,24,770,40,892,64,895,58,46,0,0,32,905,0,0,52,908,836,911,915,918,922,926,812,809,929,848,933,937,942,946,951,806,955,816,959,842,0,803,968,800,972,845,855,819,0,839,0,0,0,988,992,826,429,0,0,996,1002,770,40,892,64,895,58,664,0,0,32,905,0,0,52,908,1011,1077,0,0,0,0,0,0,0,0,0,0,0,0,0,822,471,0,1053,664,1057,46,1057,0,0,0,6,8,10,12,14,16,18,20,22,819,819,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,50,50,38,0,24,770,40,892,64,895,58,46,410,32,32,905,1084,52,52,908,44,306,36,377,106,28,321,42,1087,644,630,633,654,660,1090,62,100,30,584,50,1093,54,303,26,637,48,72,58,664,46,46,1097,66,121,79,60,38,0,0,996,0,770,40,892,64,895,58,664,410,32,32,905,1084,52,52,908,1011,0,0,1101,1105,0,0,0,0,0,0,56,1111,1115,74,1119,491,34,1124,664,1057,46,1057,0,0,0,6,8,10,12,14,16,18,20,22,0,0,0,0,0,1127,0,0,0,0,0,0,0,0,0,0,0,50,50,38,0,24,770,40,892,64,895,58,664,0,0,32,905,0,0,52,908,44,306,36,377,106,28,321,42,1087,644,630,633,654,660,1090,62,100,30,584,50,0,54,303,26,637,48,72,58,0,46,976,0,985,121,79,60,38,0,0,996,1002,770,40,892,64,895,58,664,898,0,32,905,898,0,52,908,1011,0,0,0,0,0,0,0,0,0,0,0,0,0,0,664,491,0,1124,664,1057,46,1057,0,0,0,6,8,10,12,14,16,18,20,22,58,58,1134,1134,0,0,0,0,0,0,0,0,0,0,0,0,0,863,50,884,0,24,770,40,892,64,895,58,46,0,0,773,905,0,0,776,908,44,306,36,377,106,28,321,42,1087,644,630,633,654,660,1090,62,100,30,584,50,0,54,303,26,1137,48,72,58,0,46,1057,0,66,121,992,60,38,0,0,996,1002,770,40,892,64,895,58,664,0,0,773,905,0,0,776,908,1011,0,0,0,0,0,0,0,0,0,0,0,1111,1115,74,664,471,34,1053,664,1057,46,1057,0,0,0,6,8,10,12,14,16,18,20,22,50,38,0,0,1140,0,0,0,0,0,0,0,0,0,0,0,0,50,50,38,0,24,770,40,892,64,895,58,0,410,32,32,905,1084,52,52,908,44,306,36,377,106,28,321,42,1087,644,630,633,654,660,1090,62,100,30,584,50,963,54,303,26,637,48,855,58,822,46,1057,980,66,121,79,60,38,0,0,0,0,770,40,892,64,895,58,664,410,32,32,905,1084,52,52,908,1011,0,0,1101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,664,0,0,0,0,0,0,6,8,10,12,14,16,18,20,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,50,50,38,0,24,770,40,892,64,895,58,46,0,32,32,905,0,52,52,908,44,306,36,377,106,28,321,42,1087,644,630,633,654,660,1090,62,100,30,584,50,0,54,303,26,637,48,72,58,822,46,1057,0,985,121,79,60,38,0,0,0,1002,770,40,892,64,895,58,664,0,32,32,905,0,52,52,908,1011,0,0,0,0,0,0,0,0,0,0,1144,1148,0,0,664,491,0,1124,664,1057,46,1057,0,0,0,6,8,10,12,14,16,18,20,22,0,0,0,0,0,0,0,0,0,6,8,10,6,8,10,1152,0,0,50,38,0,24,770,40,892,64,895,58,46,0,32,773,905,0,52,776,908,44,911,915,918,106,28,812,42,929,644,630,937,942,946,1090,62,955,816,959,50,1093,54,968,800,972,48,72,58,664,46,1057,1097,66,988,79,60,38,0,0,996,1002,770,40,892,64,895,58,664,0,32,773,905,0,52,776,908,1011,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,833,0,664,1057,46,1057,0,0,0,6,8,10,12,14,16,18,20,22,0,1158,1170,0,0,0,0,0,0,0,0,0,0,0,0,0,0,50,50,38,0,24,770,40,892,64,895,58,46,0,32,773,905,0,52,776,908,44,306,36,377,106,28,321,42,1087,644,630,633,654,660,1090,62,100,30,584,50,0,54,303,26,637,48,72,58,664,46,1057,980,66,121,79,60,38,0,0,0,1002,770,40,892,64,895,58,664,0,32,773,905,0,52,776,908,1011,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,664,1057,46,1057,0,0,0,6,8,10,12,14,16,18,20,22,0,0,0,0,0,0,0,0,0,0,1182,1182,1182,1182,1182,1182,0,0,50,38,0,24,770,40,892,64,895,58,46,1189,32,773,905,1198,52,776,908,44,306,36,377,106,28,321,42,1087,644,630,633,654,660,1090,62,100,30,584,50,1205,54,303,26,637,48,72,58,664,46,1057,1205,66,121,79,60,38,1216,1224,0,1232,770,40,892,64,895,58,664,1240,32,773,905,0,52,776,908,0,0,1250,1261,1267,1261,1267,1261,0,1267,0,1272,1285,1267,1293,1285,1285,1097,1299,664,1057,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1267,1267,0,0,0,0,0,0,0,0,0,0,0,0,0,1311,50,38,1314,24,770,40,892,64,895,58,46,1318,32,773,905,1321,52,776,908,44,306,36,377,106,28,321,42,1087,644,630,633,654,660,1090,62,100,30,584,50,1325,54,303,26,637,48,72,58,664,46,1057,1097,66,121,79,60,38,1328,0,0,0,770,40,892,64,895,58,905,1336,32,773,905,1348,52,776,908,1352,1361,1370,0,6,8,10,12,14,16,18,20,22,1378,1389,0,0,0,0,664,1057,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1311,50,38,1314,24,770,97,1396,40,892,64,895,58,664,46,1057,32,773,905,52,776,908,1321,0,1400,44,306,36,377,106,1403,28,321,42,1087,644,1407,1411,630,633,654,660,1090,1415,62,100,30,584,50,770,1420,54,303,26,637,48,1423,72,58,1426,46,0,0,66,121,79,60,38,1057,34,0,0,0,0,0,0,0,0,770,97,1396,40,892,64,14,895,18,58,32,773,905,52,776,908,46,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,664,1057,0,0,0,0,0,0,0,0,0,0,0,0,1429,44,306,306,306,306,306,106,650,321,321,321,321,72,30,62,100,100,100,50,30,62,100,100,100,50,26,54,303,34,303,34,303,48,72,58,58,46,46,68,60,60,60,38,46,1432,38,1432,24,24,770,1437,40,892,1440,1443,64,895,0,0,0,0,1447,637,32,97,52,905,905,1451,812,809,0,848,933,937,942,48,951,806,955,816,959,842,803,968,800,972,845,1144,1454,1148,1459,852,1464,1044,1468,855,819,839,988,992,826,429,24,1471,1476,1487,822,0,0,0,0,770,40,892,64,895,58,664,46,1057,32,773,52,776,1491,1496,40,44,306,1501,306,0,1505,106,321,1509,60,1514,0,644,0,0,836,911,915,918,30,38,100,100,0,50,26,54,303,34,303,34,955,48,72,58,803,46,800,68,845,1144,60,38,1459,852,1464,1044,24,855,770,1437,40,892,72,1124,64,895,1476,52,46,644,0,0,32,1518,52,1521,905,0,0,0,0,0,0,0,0,48,1524,1524,0,0,0,0,0,0,0,0,0,0,0,0,1529,1532,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1101,911,915,918,922,926,812,809,929,848,1535,933,937,942,946,951,806,955,816,959,842,803,968,800,972,845,855,819,839,852,826,429,976,24,1540,40,892,64,895,32,1545,52,908,1549,770,40,892,64,895,32,905,1545,1545,32,875,618,884,1011,1554,1559,1559,1559,1559,1566,44,306,36,377,106,28,321,42,20,644,630,633,654,660,1090,62,100,30,584,50,54,303,26,637,48,127,350,130,357,68,124,74,1545,72,58,46,121,1572,60,38,24,1576,58,1580,1580,1580,1588,1588,1588,770,40,892,64,895,58,664,46,1057,32,773,52,776,48,38,40,892,1540,1540,1540,1540,1540,1540,1540,1540,1540,1540,1540,1540,1596,1596,44,306,36,377,106,28,321,42,20,644,630,633,654,660,1090,62,100,30,584,50,54,303,26,637,48,127,350,130,357,68,124,74,1602,72,58,46,121,79,60,38,24,1576,68,72,58,1606,70,1611,1616,1620,1623,482,1627,1631,0,0,0,0,0,0,0,0,0,0,603,1635,1639,1643,410,1647,1651,1655,609,1659,1663,1667,1671,1084,1675,1679,1602,331,1684,606,1688,1693,1698,1703,1707,1712,1717,1721,1725,1606,1729,1611,1616,1620,1623,482,1627,1631,1733,1736,1739,1639,587,0,1745,0,0,0,44,306,36,377,106,28,321,42,1087,644,1754,630,633,654,660,1090,630,100,30,584,50,54,303,26,637,48,72,58,46,68,60,38,1057,24,0,40,892,64,895,32,0,52,908,0,770,40,892,64,895,32,905,0,0,0,50,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,50,58,46,32,121,79,58,664,46,1057,58,664,46,1057,0,0,0,0,0,0,1758,24,97,855,475,1765,32,1768,1772,52,852,1775,109,1318,64,1779,482,1783,670,1786,347,40,1789,1793,1797,1802,1808,1813,1818,1824,1830,1836,1841,1845,1850,1854,1858,1864,1871,1878,1883,1888,1892,1897,1905,1910,1914,1919,1925,1930,1937,1943,1948,1953,1958,1964,1969,1973,1978,1982,1986,1991,1999,2005,24,26,36,30,32,66,74,62,40,44,46,48,50,52,54,124,58,60,62,64,54,44,36,56,121,321,28,74,28,321,70,42,38,32,72,68,2014,109,2017,2030,2041,2055,2069,2083,2096,2114,2126,2139,24,26,36,30,32,66,74,62,40,44,46,48,50,52,54,124,58,60,62,64,54,44,36,56,121,321,28,74,28,321,70,42,38,32,72,68,2014,109,34,2158,2170,2182,0,0,0,0,0,0,36,2194,50,30,654,58,48,26,1137,60,79,2197,42,2201,28,44,62,54,38,106,1090,1420,2204,2207,2210,664,491,2210,1423,2213,2216,2219,2222,2226,2229,2233,2237,2241,2245,2249,2252,2255,2258,2219,2261,2265,2268,2271,2274,2277,2280,2283,2287,60,2291,2294,2297,315,318,121,2300,2305,327,2309,74,36,30,48,26,60,74,2314,42,28,62,54,50,42,0,0,2318,2322,2326,321,2329,2334,2338,2341,762,56,2344,2349,2355,2361,0,0,800,24,97,855,475,1765,32,1768,1772,52,852,1775,109,1318,64,1779,482,1783,670,1786,347,40,1789,1793,1797,1802,1808,1813,1818,1824,1830,1836,1841,1845,1850,1854,1858,1864,1871,1878,1883,1888,1892,1897,1905,1910,1914,1919,1925,1930,1937,1943,1948,1953,1958,1964,1969,1973,1978,1982,1986,1982,64,2366,1910,2371,895,770,773,32,52,852,36,2194,2375,50,136,2378,30,46,2381,2384,2387,162,2390,2393,2396,48,26,2226,60,79,106,42,28,44,62,54,38,2399,2402,106,1420,2406,2409,2412,2207,2415,2418,2422,2425,2428,1057,2432,2436,2440,2444,2448,2452,153,2456,2459,2462,2465,1423,2468,2471,2475,2478,2481,2213,2484,2258,303,2341,2265,2271,2487,2280,74,36,79,1318,306,50,2406,2409,2338,2341,1529,2490,1532,2493,56,2496,2500,2505,2510,2514,2518,429,2523,2526,2529,2533,1620,2537,2540,839,2544,2547,2197,2550,2554,1426,2557,2561,2565,2569,2573,2578,2583,2587,2591,845,2596,2599,2602,2606,2610,2613,2616,2222,2620,2624,2628,2633,2638,2642,2646,819,2651,2654,2657,2661,2665,2668,2671,826,2675,2678,2681,2685,2689,1325,2692,988,2696,2700,2704,2300,2305,327,2309,479,2709,2712,2715,2719,2723,2726,2729,2734,2738,2741,2745,2750,2755,2759,2762,2318,2322,2766,2770,2329,2334,2775,2779,2783,2788,2344,2349,2355,2361,0,0,800,2792,2795,2798,2802,2806,2809,2812,985,2816,2819,2822,2826,2830,2833,2836,806,2840,324,2843,2847,2851,2854,2857,926,2861,2864,2867,2871,2875,2878,2881,2885,2888,2891,2894,2898,2902,2905,2908,2912,2916,2920,2924,2929,2934,2938,2943,842,2947,2950,785,2953,2957,1400,2960,848,2964,2968,2972,2977,2982,2986,2990,24,32,40,52,64,66,915,836,836,2995,2998,3001,3005,3009,1311,2526,3012,2523,3016,3020,3025,3030,1426,2544,3034,3038,3042,3046,3051,3056,3060,3064,3068,2957,3073,3078,3084,3090,3095,3099,852,3103,1783,3106,3110,482,3114,2678,1325,2675,3117,816,806,432,2851,3120,1044,3123,3126,3129,3133,464,3137,3140,1464,3144,3148,3152,3157,3162,3166,3170,855,670,347,3175,3179,1772,1318,855,816,3183,3120,3186,3190,432,3194,3197,942,3201,2496,2500,2505,2510,2514,2518,809,3205,3208,3211,3215,3219,3222,2540,915,3225,3228,3231,3235,3239,3242,0,3245,0,3249,3253,3258,3263,0,0,1060,3267,3271,3275,3280,3285,3289,0,955,3293,3297,3301,3306,3311,1321,3315,812,3320,3324,3328,3333,3338,3342,3346,968,3351,3355,3359,3364,3369,3373,3377,1144,3382,3386,3390,3395,3400,3404,3408,3413,3417,3421,3425,3430,3435,3439,0,833,2738,1733,3443,3447,3451,2759,2762,803,3454,3457,3460,3464,3468,3471,2779,3474,2788,3478,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,32,40,52,64,66,915,836,3239,3228,3242,3225,3482,429,1620,2526,2537,2523,112,839,2554,2547,1426,2544,3485,845,2610,2599,2613,2596,842,3064,3488,2957,2950,1400,2947,3492,3095,3099,3495,3499,3503,3507,826,60,2689,2678,1325,2675,3117,816,806,432,2851,3120,324,3194,3183,3511,3514,3518,3522,3526,3530,3534,3538,1144,3400,3386,3404,3382,3542,852,482,1783,3114,3103,3546,855,1772,347,1318,670,3549,0,0,0,0,0,0,0,0,0,0,0,0,32,3552,40,892,52,776,776,773,40,24,770,482,482,1783,1783,3556,3556,3114,3114,3560,3560,3560,852,852,3106,3106,3106,905,68,0,62,44,121,60,50,68,50,0,68,28,0,46,410,609,1084,603,3468,3564,3457,3569,3471,3573,3573,2533,2526,803,3460,2943,2943,3577,3577,3581,3581,3586,3586,3590,3590,2779,2779,3595,3595,3595,54,54,38,2851,3600,324,3605,2854,3609,3609,3190,3120,806,2843,3613,3613,3617,3617,3621,3621,3626,3626,3630,3630,2857,2857,3635,3635,3635,62,3640,3644,3648,933,3009,3652,2998,3657,1311,3661,3661,836,3001,3030,3030,3016,3016,3665,3665,3670,3670,3674,3674,3012,3012,3020,3020,3020,44,3679,3682,3686,3690,3694,2875,3698,2864,3703,2878,3707,3707,926,2867,3711,3711,3715,3715,3719,3719,3724,3724,3728,3728,2881,2881,3733,3733,3733,28,100,2610,3738,2599,3743,2613,3747,3747,845,2602,3751,3751,3755,3755,3759,3759,3764,3764,3768,3768,2616,2616,3773,3773,3773,48,48,2481,48,48,2957,3778,2950,3783,1400,3787,3787,842,785,3791,3791,2960,2960,3795,3795,3795,50,106,2378,2554,3800,2547,3805,1426,3809,3809,839,2197,3813,3813,3817,3817,3821,3821,3826,3826,3830,3830,2557,2557,3835,3835,46,46,46,2689,3840,2678,3845,1325,3849,3849,826,2681,3853,3853,3857,3857,3861,3861,3866,3866,3870,3870,2692,2692,3875,3875,3875,60,60,3880,60,2297,3883,3880,3887,3892,3897,3902,2305,2700,3907,327,3912,988,2704,3917,3917,3922,3922,3927,3927,3933,3933,3938,3938,2309,2309,3944,3944,121,1772,3950,347,3955,1318,3959,3959,855,3175,3963,3963,3967,3967,3971,3971,3976,3976,3980,3980,3985,3985,3989,3989,3989,72,72,72,347,2665,2665,2554,3994,2654,3999,2668,4003,1426,819,2657,839,4007,4007,58,58,58,3451,4012,1733,4017,2759,4021,833,3443,4025,4025,34,3311,3311,3297,3297,4030,4030,1321,4035,955,3301,4040,4040,100,4046,4051,4056,937,633,4061,4065,4069,4073,1620,2526,4077,2537,4081,429,2529,38,38,4085,4088,2712,4093,2726,4097,479,2715,56,4101,4106,4111,4116,2665,2654,2668,819,4121,4127,4131,4136,4140,922,4145,106,1403,2305,2700,327,988,3311,3297,1321,955,100,4150,4154,4159,4163,467,4168,2396,3311,3297,4030,1321,4035,955,3301,100,26,32,40,52,24,482,1783,3114,852,2957,2950,1400,842,3009,2998,1311,836,1620,2526,2537,429,4173,4177,425,4181,4186,918,2651,2668,2665,2661,2654,819,3103,3114,482,3110,1783,852,4190,4194,4198,4202,4207,4211,3293,1321,3311,3306,3297,955,4215,3648,3640,4219,3644,933,3454,3471,3468,3464,3457,803,54,3225,3242,3239,3235,3228,915,4224,1314,4228,4232,4237,911,4241,4245,4249,4253,4258,1487,4262,2947,1400,2957,2953,2950,842,2596,2613,2610,2606,2599,845,670,1318,1772,3179,347,855,3205,3205,3222,3219,3215,3208,3208,809,4265,4269,4273,4277,4282,1064,2544,1426,2554,2550,2547,839,4286,4290,4294,4298,4303,3514,4307,4159,4311,4315,4150,467,4320,4111,4101,4325,4106,4116,3534,3530,3522,4331,3526,3518,3123,3137,464,3133,3126,1044,74,74,4336,4340,4344,4348,4353,1148,2675,1325,2689,2685,2678,826,2696,327,2305,2300,2700,988,121,3382,3404,3400,3395,3386,1144,3320,3342,3338,3333,3324,812,4357,4362,4367,4372,4378,4383,70,839,4388,4392,4397,4402,4408,4413,4419,4424,0,0,0,0,0,0,0,0,0,836,26,46,34,60,50,38,30,62,28,56,48,36,106,74,58,24,52,64,32,40,321,100,303,54,70,54,1426,2833,988,4430,826,34,66,64,118,72,68,100,100,24,52,4434,97,52,52,52,109,1084,58,44,28,44,36,106,36,36,68,38,38,38,38,50,50,50,40,32,42,36,97,24,1765,54,74,60,60,60,28,74,62,62,30,26,26,54,54,32,48,48,48,46,46,106,106,30,52,4437,4441,2709,2709,2709,60,118,118,118,56,70,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,4445,0,0,0,0,0,0,0,0,0,6,8,10,12,14,16,18,20,22,0,0,0,0,0,0,24,32,40,52,64,109,1440,773,842,4454,800,803,479,915,845,839,826,988,806,816,812,809,855,819,852,833,836,911,1144,1044,2529,4458,467,3148,3324,4462,4462,4462,4462,4462,4462,4462,4462,4462,4462,4462,4462,4462,4462,4462,4462,4462,4462,4462,4462,4462,4462,4462,4462,4462,4462,4467,4467,4467,4467,4467,4467,4467,4467,4467,4467,4467,4467,4467,4467,4467,4467,4467,4467,4467,4467,4467,4467,4472,4472,4472,4472,4472,0,0,0,0,0,0,0,0,44,306,36,377,106,28,321,42,1087,644,62,633,30,660,1090,62,100,30,584,50,54,303,26,637,48,72,58,46,66,121,79,60,38,46,56,24,770,40,892,64,4479,895,4482,4486,4489,4493,4496,32,905,776,776,908,24,770,770,40,892,72,1124,64,895,4500,109,855,335,32,97,905,776,908,48,38,24,0,0,0,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4503,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,836,911,915,918,922,926,812,809,929,4506,806,955,816,959,842,803,968,800,972,845,855,819,839,852,988,992,826,429,0,0,0,24,40,64,773,905,776,908,32,52,855,819,852,0,0,0,0,836,922,875,806,842,803,845,819,839,4510,4520,4530,0,0,0,0,3809,0,0,0,0,0,0,6,8,10,12,14,16,18,20,22,836,2885,922,1144,826,855,806,955,839,803,968,845,833,985,429,479,911,1454,842,24,40,773,4535,64,776,52,1440,32,4538,905,0,0,4542,4542,4542,4542,4542,0,0,0,0,0,0,0,0,0,0,0,179,479,179,179,179,836,2885,922,179,179,179,1144,826,855,179,179,179,806,955,842,179,179,179,803,968,845,179,179,179,833,985,839,179,179,179,429,816,800,179,179,4548,4552,0,0,0,0,0,0,4556,770,892,64,895,32,97,52,782,1440,1521,4562,4566,507,4569,4573,4577,4580,4580,4580,4580,4580,4580,4580,0,0,0,0,0,0,0,0,0,6,8,10,12,14,16,18,20,22,0,0,0,0,4586,4590,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,836,915,922,4595,803,800,845,4600,806,816,842,4604,926,809,848,4608,855,819,839,985,826,24,429,40,64,32,52,97,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4613,4613,4617,4623,4630,4636,4636,4642,4642,4648,4648,819,819,839,839,4654,4660,4667,4667,836,836,915,915,922,926,926,809,809,848,806,806,816,816,842,806,806,816,816,842,803,803,800,800,845,855,819,839,852,826,826,826,429,4673,4681,4613,4613,4688,4688,819,819,839,839,4693,4693,4693,4693,4700,4700,0,752,4706,4711,4716,2830,4719,4723,0,0,0,0,0,6,8,10,12,14,16,18,20,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4730,4739,4749,24,40,64,97,52,32,1786,836,479,915,922,926,809,1044,848,806,816,842,803,833,985,800,845,855,819,839,852,826,2885,429,4759,4769,4778,4786,4795,4803,4814,4823,4831,4842,0,0,0,911,4850,0,6,8,10,12,14,16,18,20,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,836,4854,911,915,4858,922,926,812,809,848,806,955,816,842,803,4862,968,833,4866,800,4870,845,4874,1144,1454,1148,855,819,839,429,4878,985,826,988,852,24,855,819,770,40,52,776,64,895,32,44,48,46,50,54,58,62,4882,4890,4895,996,0,0,0,0,0,0,0,0,0,6,8,10,12,14,16,18,20,22,0,0,0,933,937,942,0,6,8,10,12,14,16,18,20,22,839,4899,4902,4454,4905,2197,4908,4912,4916,4920,2547,4924,4927,4930,4934,2544,4937,4940,4943,4566,2554,4947,4950,410,4954,1426,4958,4962,4965,4968,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4971,4971,97,4971,4971,4971,4971,4971,4979,40,4971,4971,4971,4971,4971,4971,4971,4984,4984,4984,109,4971,4993,4997,4971,4971,4971,4971,4971,4984,4984,4984,4971,4971,4971,4971,5004,587,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,4971,24,5011,5018,5011,26,30,32,5024,4979,5011,36,5011,44,48,5030,52,4979,4993,4997,54,62,64,4984,5011,66,587,5034,5039,5045,5039,3324,40,58,64,66,5034,5051,5057,3355,3324,1440,26,30,34,48,50,54,58,58,60,62,74,36,5061,5070,100,40,5078,54,64,5083,26,30,34,36,44,46,48,50,54,58,60,5091,66,70,74,24,5018,30,32,32,4979,5024,40,52,5091,64,5095,5011,28,28,5099,5103,34,5112,5120,5011,40,5078,4971,4971,42,46,46,4971,48,5011,50,50,4971,5127,3355,60,5091,62,64,5083,4971,66,5011,74,74,74,5095,5134,0,0,0,0,0,0,0,0,0,0,58,0,0,0,0,0,0,0,0,5140,97,1451,5150,28,5070,5099,36,4971,44,46,4971,4971,50,4971,4971,58,60,60,74,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,24,26,26,26,26,26,26,28,28,30,30,30,30,30,30,30,30,30,30,32,32,32,32,32,32,32,32,32,32,34,34,24,32,40,52,64,52,64,773,50,106,26,54,56,36,48,46,60,121,62,30,321,42,72,58,68,34,44,911,127,74,38,5153,2396,124,321,48,32,40,52,64,52,64,106,26,54,56,36,48,62,30,321,42,127,72,68,44,36,38,5156,644,130,32,40,4577,64,64,106,44,36,38,54,121,62,30,42,34,36,38,127,74,58,321,124,40,44,58,34,124,64,64,64,64,66,66,66,66,68,38,70,68,48,68,68,24,40,44,106,28,630,633,654,1090,62,30,54,303,79,124,74,24,62,124,377,106,28,1087,933,660,62,584,79,5159,124,74,64,72,637,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,40,40,40,40,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,64,64,64,64,64,64,64,64,64,64,64,64,64,64,72,72,72,72,72,72,72,72,5162,5162,5162,5162,72,72,5018,5018,5018,5018,5018,5018,5018,5018,5018,5018,5018,5018,5018,5018,5018,5018,5175,5175,5175,5175,5175,5175,0,0,5175,5175,5175,5175,5175,5175,0,0,5183,5183,5183,5183,5183,5183,5183,5183,5183,5183,5183,5183,5183,5183,5183,5183,5078,5078,5078,5078,5078,5078,5078,5078,5078,5078,5078,5078,5078,5078,5078,5078,5187,5187,5187,5187,5187,5187,0,0,5187,5187,5187,5187,5187,5187,0,0,5083,5083,5083,5083,5083,5083,5083,5083,0,5083,0,5083,0,5083,0,5083,5195,5195,5195,5195,5195,5195,5195,5195,5195,5195,5195,5195,5195,5195,5195,5195,5018,5018,5175,5175,5183,5183,5078,5078,5187,5187,5083,5083,5195,5195,0,0,5018,5018,5018,5018,5018,5018,5018,5018,5018,5018,5018,5018,5018,5018,5018,5018,5183,5183,5183,5183,5183,5183,5183,5183,5183,5183,5183,5183,5183,5183,5183,5183,5195,5195,5195,5195,5195,5195,5195,5195,5195,5195,5195,5195,5195,5195,5195,5195,5018,5018,5018,5018,5018,0,5018,5018,5018,5018,5018,5018,5018,0,0,0,0,0,5183,5183,5183,0,5183,5183,5175,5175,5183,5183,5183,0,0,0,5078,5078,5078,5078,0,0,5078,5078,5078,5078,5078,5078,0,0,0,0,5083,5083,5083,5083,5057,5057,5083,5083,5083,5083,5083,5083,5057,0,0,0,0,0,5195,5195,5195,0,5195,5195,5187,5187,5195,5195,5195,0,0,0,5201,0,2,2,2,2,5205,2,2,5209,5213,2,2,2,2,2,2,2,2,2,2,0,0,0,5218,1,1,1,1,1,1,1,1,1,1,1,0,0,0,2,0,0,5224,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,5230,0,0,2,0,0,0,0,0,0,5234,0,1,1,0,0,5237,0,0,5241,0,0,0,0,0,0,5248,0,0,0,5255,2,2,0,0,0,0,0,0,0,0,0,0,5261,0,0,5267,5267,5274,40,0,5280,0,0,0,0,0,0,0,5285,0,0,0,50,0,0,0,0,0,0,0,0,0,0,5291,0,0,0,0,0,24,32,52,70,5024,0,0,0,0,0,0,0,0,0,5296,0,0,0,0,0,0,0,0,5301,0,0,0,0,0,0,0,0,0,5305,0,5309,0,0,0,5314,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5320,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5325,0,0,0,0,0,0,0,0,0,0,0,0,0,5329,0,0,0,0,0,0,0,0,0,0,4971,5333,0,0,0,4971,0,0,0,0,2,0,0,0,0,0,4971,0,0,0,0,5337,2,0,0,0,0,0,4971,5078,0,0,0,4971,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5342,0,0,5347,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5352,0,0,0,0,0,0,0,0,0,0,5358,0,0,0,0,0,0,0,0,0,0,5325,0,0,0,0,0,0,0,5362,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5367,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5372,0,0,0,0,0,0,0,0,0,5320,0,0,0,0,5377,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5381,0,0,0,0,0,0,0,5386,0,0,0,0,0,0,0,0,0,5391,0,0,0,0,0,0,0,0,0,5396,5401,5401,0,0,0,5407,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5413,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5419,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5426,5386,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5432,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5248,0,0,5437,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5442,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5448,0,0,0,0,0,0,0,0,0,0,0,0,0,5453,5459,0,0,0,0,0,0,0,5453,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5248,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5464,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5471,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5476,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5325,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,24,26,26,26,26,26,26,28,28,30,30,30,30,30,30,30,30,30,30,32,32,32,32,32,32,32,32,32,32,34,34,36,36,38,38,38,38,38,38,38,38,38,38,40,40,40,40,44,44,44,44,44,44,46,46,46,46,46,46,46,46,48,48,48,48,48,48,50,50,50,50,50,50,50,50,52,52,52,52,52,52,52,52,54,54,54,54,58,58,58,58,58,58,58,58,60,60,60,60,60,60,60,60,60,60,62,62,62,62,62,62,62,62,64,64,64,64,64,64,64,64,64,64,66,66,66,66,68,68,68,68,68,68,68,68,68,68,70,70,70,70,72,72,74,74,74,74,74,74,38,62,68,72,24,60,24,26,28,30,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,40,40,40,40,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,64,64,64,64,64,64,64,64,64,64,64,64,64,64,72,72,72,72,72,72,72,72,16,18,20,22,0,0,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,32,32,32,32,32,32,0,0,32,32,32,32,32,32,0,0,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,52,52,52,52,52,52,5386,0,52,52,52,52,52,52,2,5480,64,64,64,64,64,64,64,64,0,64,0,64,0,64,0,64,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,24,24,32,32,32,32,40,40,52,52,64,64,52,52,0,0,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,52,24,24,24,24,24,0,24,24,24,24,24,24,24,0,40,0,0,0,32,32,32,0,32,32,32,32,32,32,32,0,0,0,40,40,40,40,0,0,40,40,40,40,40,40,0,0,0,0,64,64,64,64,58,58,64,64,64,64,64,64,58,0,0,0,0,0,52,52,52,0,52,52,52,52,52,52,52,0,0,0,5485,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5491,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,5496,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5501,0,0,0,0,0,0,0,0,0,0,5506,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5509,0,5255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5514,0,0,0,5519,50,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5524,0,0,0,0,0,5529,0,0,0,0,0,5534,0,0,0,5540,5544,5547,5550,46,5553,50,5557,1134,68,2406,30,1786,44,62,5561,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5564,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5568,0,5573,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5578,0,0,0,0,5564,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,40,892,5583,5587,66,2819,5590,5594,5599,70,2891,5602,46,28,30,48,40,892,5583,5587,66,2819,5590,5594,5599,70,2891,5602,46,28,30,48,6,30,10,12,14,16,18,20,22,0,6,8,10,12,14,16,18,20,22,0,0,0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,66,0,0,66,0,0,0,0,0,5606,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5612,0,0,0,0,5617,0,0,0,0,0,0,0,0,0,5622,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5629,0,0,0,0,5291,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5635,0,0,0,5629,0,0,0,0,5642,0,0,0,0,0,0,0,0,0,0,0,0,0,5647,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5651,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5325,5655,0,0,0,0,0,0,0,0,5285,0,0,0,5367,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5661,5661,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5666,0,0,0,0,5671,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,0,0,5676,5682,0,0,0,0,5688,0,0,0,0,0,0,0,0,0,0,5693,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5699,0,0,0,0,0,0,5291,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5476,5704,5704,0,0,0,0,0,0,0,0,0,0,0,5708,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5712,0,0,5717,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5721,5721,5721,5721,5721,5721,5721,5721,5721,5721,5721,609,609,0,5721,5721,0,0,609,609,5721,5726,0,609,609,609,0,5464,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5693,0,0,0,0,0,0,0,0,5730,0,0,2,2,0,0,0,0,0,0,0,0,5721,0,0,0,0,0,0,0,0,0,0,5682,5682,5682,0,0,0,5721,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5721,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,272,272,272,272,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,5682,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5735,5739,5744,5749,5757,5763,5769,5777,5783,5790,5795,40,5803,5810,5815,5823,5831,5837,5841,5848,5854,5860,5867,5871,5877,5882,3468,5886,3386,5891,988,5898,5903,5908,5913,670,5921,5921,1318,5925,5933,5925,5937,5942,5950,5958,5969,0,5735,5739,5744,5749,5757,5763,5769,5777,5783,5790,5795,40,5803,5810,5815,5823,5831,5837,5841,5848,5854,5860,5867,5871,5877,5882,3468,5886,3386,5891,988,5898,5903,5908,5913,670,5921,5921,1318,5925,5933,5925,5937,5942,5950,5958,5969,0,46,46,46,54,58,24,62,38,38,44,44,74,74,5018,48,24,0,66,68,68,66,1432,1432,5978,32,58,52,4971,42,4971,0,0,5987,5987,5992,5992,5051,5051,5997,5997,6003,6003,6007,6007,6011,6011,6016,6016,6021,6021,6028,6028,6034,6034,6039,6039,2599,2599,2950,2950,6045,6045,52,52,3457,3457,2668,2668,6049,6049,6054,6054,4500,4500,1733,1733,4237,4237,6058,6058,6062,6062,6066,6066,6076,6076,6080,6080,6066,6066,6066,6066,6080,6080,6076,6076,283,283,6094,6094,6076,6076,6076,6076,6102,6102,6066,6066,6076,6076,6076,6076,6111,6111,6076,6076,6076,6076,6076,6076,6076,6076,6076,6076,6076,6076,6076,6076,6076,6076,6076,6076,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,603,1635,1639,1643,410,1647,1651,1655,609,1659,1663,1667,1671,1084,1675,1679,1602,331,1684,606,1688,1693,1698,1703,1707,1712,1717,1721,1725,1606,1729,1611,1616,1620,1623,482,1627,1631,0,0,0,0,0,0,0,0,0,0,855,6120,6124,6129,6133,6139,6146,6150,6154,6159,6164,6170,6174,6178,6182,6189,6195,6139,6182,6199,3175,6204,6182,6209,6182,347,6213,6218,6182,6226,6230,4506,6182,6182,6234,670,1447,6238,6243,6182,6248,6253,6257,6262,6267,6271,6276,6281,6286,6290,6294,6298,6302,6313,0,0,0,0,0,0,0,0,0,6318,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6332,6336,6340,6344,6348,6353,6357,6361,6365,6369,782,6374,6378,6382,6387,6391,6396,6401,6406,6410,6415,6420,6426,0,0,0,0,0,0,0,0,0,992,6431,6435,6439,6444,6449,4430,0,6453,6457,6461,6465,6470,6475,6479,0,6483,6487,6491,6495,6500,6505,6509,0,6513,6518,6523,6528,6534,6540,6545,0,6550,6554,6558,6562,6567,6572,6576,0,6580,6584,6588,6592,6597,6602,6606,0,6610,6614,6618,6622,6627,6632,6636,0,6640,6644,6648,6652,6657,6662,6666,0,2806,2830,425,432,3162,464,836,407,413,410,52,3468,6670,6673,2851,429,3400,3338,988,6676,5937,6682,24,335,6688,6694,6267,670,6704,6713,5933,6704,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,220,5682,220,5682,6720,6725,6725,6725,6725,6725,6725,6734,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,24,24,40,40,64,64,32,32,52,52,836,915,2998,3228,2995,3225,3009,3239,1311,3242,826,1044,2678,3126,2675,3123,2689,464,1325,3137,806,816,324,3120,2840,2840,3183,2851,432,2854,3194,842,2950,2947,2957,1400,429,800,803,2526,70,3457,2523,2792,3454,1620,2806,3468,2537,2809,3471,845,2599,2596,2610,2613,855,855,670,670,1318,1318,819,2654,2651,2665,2668,852,852,1783,482,3114,50,2816,836,3009,0,0,0,0,0,0,0,0,0,2,24,24,40,40,64,64,32,32,52,52,836,915,2998,3228,2995,3225,3009,3239,1311,3242,826,1044,2678,3126,2675,3123,2689,66,66,66,66,816,324,3120,2840,2840,3183,2851,432,2854,3194,842,2950,2947,2957,1400,429,800,803,2526,2795,3457,2523,2792,3454,1620,2806,3468,2537,2809,3471,845,2599,2596,2610,2613,855,855,670,670,1318,1318,819,2654,2651,2665,2668,852,52,1783,482,3114,50,2816,836,3009,985,2819,2830,2833,0,0,0,0,0,0,0,0,0,0,26,54,48,34,30,62,50,46,36,44,38,42,56,70,124,321,121,58,74,28,60,24,52,32,4535,905,1518,908,139,603,410,4454,5030,6670,40,64,371,66,106,6743,4927,0,0,0,6746,6753,6765,6777,6783,6795,6807,6814,6826,6832,6845,6857,6869,6880,6894,6908,6920,6926,6932,6943,6954,6959,6969,6975,6981,6992,7000,7008,7016,7024,24,97,855,475,1765,32,1768,1772,52,852,1775,109,1318,64,1779,482,1783,670,1786,347,40,0,7030,2017,2030,2041,2096,2126,7041,7058,7072,7090,7102,7113,7127,7141,7154,7167,7185,7203,7215,7229,7243,7262,7274,7285,7297,7308,7319,7327,7338,7347,7361,7378,7394,2182,1858,1864,1883,1930,1937,1948,1999,7405,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2792,3126,3208,3225,773,7412,776,7416,4934,7420,7424,4943,7428,7431,7435,7440,1437,1429,7445,7449,54,62,44,38,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2995,2678,2675,2854,2947,429,2526,2523,1620,2537,2596,819,2654,2651,2665,2668,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7454,52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7460,7467,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,0,26,0,44,0,46,0,28,40,34,0,48,60,54,0,32,0,38,0,52,0,58,0,30,42,36,0,50,62,56,0,0,0,0,0,64,0,66,0,0,0,0,0,70,0,0,0,0,0,0,0,74,0,0,0,0,68,0,0,72,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5688,5606,0,0,7473,7478,7482,0,0,0,0,0,7487,0,0,0,0,0,0,0,0,0,7491,0,0,0,0,0,5442,0,0,0,0,7495,0,0,5688,0,0,0,7500,5426,0,7504,7511,7516,7521,5218,7528,5655,5491,7478,7533,0,0,7538,7542,5476,7548,7555,7558,0,0,0,0,5314,7565,5708,7478,7571,0,7576,7582,7588,5476,7592,7599,5666,5386,7605,0,0,0,0,7500,0,0,0,5381,5459,7495,7588,5476,7610,7616,5413,0,0,0,5347,7622,5476,5301,5325,7628,7632,7637,0,0,0,7643,7500,5296,7648,0,7576,0,7653,5237,7658,7663,5296,7667,7538,7672,5629,7675,7681,7687,7628,7692,7696,7700,7706,7710,7715,7653,5629,7719,0,7723,5666,7730,0,0,5296,7734,7738,5320,0,7742,7658,0,7746,7752,7757,7628,7763,7667,5453,5661,5337,0,0,5617,0,5666,5301,0,7632,0,5325,0,0,0,0,7768,0,7774,7779,7786,7790,0,7795,5342,0,0,7790,7800,0,5391,5301,0,7599,5568,7805,5655,0,0,0,5224,7811,7582,7817,0,0,5401,7592,5329,5426,0,7823,5661,0,5358,5381,7588,0,0,0,5337,5426,7706,0,7828,5564,7632,5280,5381,0,0,7834,7839,0,7843,0,0,7849,0,5301,5396,7853,7858,0,7667,7864,7867,5476,5291,7872,5407,0,7710,5248,7710,0,7672,0,7516,5291,7588,7675,7877,0,0,7605,7883,5407,7576,7888,5661,5301,0,7710,5237,5407,0,5325,0,7893,5573,7898,5568,0,7904,7779,7908,0,7849,0,5291,5666,7914,7920,7924,7672,7930,7864,5329,0,0,7935,7495,7940,7946,7628,0,7952,5476,5274,7817,7957,0,5333,5391,0,7962,5642,5381,7967,0,5699,7482,0,7972,7977,7982,7986,5730,7990,0,5485,7663,0,7864,5573,5661,5573,7994,0,0,7811,7588,5337,0,7667,7999,7500,5391,0,0,8005,0,7849,5671,7834,5688,5476,5396,0,8010,8015,7883,7565,8022,5708,5564,8028,7849,5337,5396,0,8033,8039,8044,5396,5301,8048,8052,8057,0,7487,0,0,5333,7516,5476,5730,0,7500,5726,7696,0,0,0,0,0,0,0,8028,7994,5661,5377,8062,8068,8073,8077,7482,0,0,0,0,0,0,8082,8087,7904,7491,5325,7853,7653,7592,7582,7752,5329,5448,8039,8093,0,5606,8099,0,0,0,0,0,0,0,7491,5391,5529,5501,8104,8110,7888,8115,7864,5661,0,7864,8119,7752,7800,7982,5717,5367,7800,5358,5325,5730,7972,0,0,0,0,8126,7663,0,0,8131,7883,8126,8073,8136,5301,7675,5519,0,0,0,0,0,7582,5377,8140,8144,8149,7972,7663,5237,0,7867,0,0,0,0,5573,5325,8154,7738,7834,7500,0,8159,0,0,0,0,7491,7521,0,5367,7571,5476,7839,0,0,5274,7658,8164,5476,7972,5476,5274,8170,5325,7653,0,8174,5301,0,5347,8179,7653,7834,0,0,7834,7653,0,7710,8184,8154,0,5358,5367,5534,8190,0,5337,8196,7500,8028,5358,8052,5485,5708,5708,0,0,0,0,8170,0,7571,8201,5381,7663,8205,7839,8048,0,0,0,0,8077,7511,8211,8217,0,5358,8222,0,5573,7653,5606,8227,0,0,0,0,5564,7888,7628,7491,8005,7653,7908,0,7908,0,7610,0,8201,7710,5661,8077,7843,5476,0,7800,8015,5367,8234,0,0,5325,8239,7491,5325,7482,0,8245,5476,5661,8052,5218,7653,7877,7658,5629,7972,8222,8249,7952,7877,5666,0,5237,0,0,8255,8261,8267,0,7610,5564,0,0,7786,0,0,5396,8039,0,5524,8272,0,5261,8277,8077,5564,5524,7867,8282,0,0,5237,7500,8005,8249,8287,0,5325,8293,7675,5476,7628,5476,7834,0,8297,8052,8302,0,5337,0,7528,5358,0,7828,0,8201,0,7487,7696,0,7790,8267,0,0,8255,0,8174,0,0,7495,5381,0,7696,7888,0,0,0,7888,0,0,7867,0,7742,5448,8179,8307,0,5352,5476,0,7571,8312,7867,8316,7528,5337,7930,7610,0,0,0,0,0,0,7738,5347,7800,8322,7576,7834,0,5329,0,5514,7746,0,7872,8048,5496,5386,0,5442,0,5320,7858,0,8005,8302,0,5274,7972,0,0,0,5401,8329,7500,5651,5296,7715,0,8334,5476,8131,5629,7706,8267,5320,5564,5476,8245,0,5401,8340,8179,8170,5329,7588,5362,5708,8345,7542,0,0,0,0,8297,7914,8349,7653,8354,7696,5708,5274,7538,0,0,5347,0,8354,7582,5534,8359,8249,5453,5509,5651,8077,5726,8365,5401,7972,5209,8370,7500,8052,8329,7972,8010,8104,8131,0,8104,8375,7605,8087,8239,5381,5661,5693,5381,8164,8211,0,5717,5642,8381,5476,7843,5218,7482,0,7588,0,7516,8387,7790,8391,8397,7628,7491,8131,5651,7667,7752,0,0,7828,7610,0,5426,8402,8255,7746,8249,8196,0,5509,8407,7663,0,8411,8417,5401,5309,0,7805,7571,8073,0,5642,8422,5325,0,8039,5325,7994,0,0,7500,8184,8267,0,7904,5578,8170,7994,5237,7706,5476,5661,7839,5688,8068,5453,7706,5325,0,0,0,7828,8149,8427,0,0,7482,8433,5325,5296,5437,5347,5386,8170,8196,5491,0,5381,7500,0,5218,8307,7710,0,8438,5464,5342,7990,8443,8052,7558,5529,8447,7710,5401,8345,7710,0,7521,7924,5396,8451,5688,8149,5564,8217,8211,7843,5480,7653,8211,8073,0,7706,7653,0,7610,0,7681,8149,7706,8365,5651,8456,8427,7521,8461,0,7706,0,5329,0,8267,7864,7768,8438,7994,7768,5320,8438,0,0,8391,5642,5291,5301,7908,8461,8467,8245,0,8149,7957,7542,5708,8473,0,8211,8479,0,5708,0,5237,5564,0,8484,8489,8316,5726,7864,0,5325,5325,5396,5285,8397,8473,8249,8126,7994,5325,5708,7628,8052,8217,8494,8447,8498,0,5301,5329,5453,5396,8277,0,7672,0,5651,7628,0,8170,0,0,8427,0,5476,0,5325,5358,0,8504,0,8010,5717,7692,5381,7622,0,0,8316,0,7839,5358,8509,5301,0,7962,7495,0,5325,8514,5274,5280,7864,7790,0,0,7853,0,8255,0,0,7478,0,8519,5377,5401,5358,0,8211,5529,8527,5666,0,5241,5573,7800,7610,0,7482,0,5241,7542,8375,8077,5476,0,5325,5325,8533,7628,8484,7811,8539,0,8093,5325,8543,7687,7839,7883,8239,5476,7999,5274,7710,7839,7710,7834,7843,8307,7610,7877,0,8422,5496,7663,7710,0,7616,5496,7853,0,8422,5496,5642,8548,0,0,8255,5401,8196,5329,7834,0,0,5476,7877,7592,5419,0,5485,0,7582,0,5676,7653,8539,0,0,8062,0,7710,8438,5358,0,8329,5329,0,5617,5358,0,5358,0,5274,7994,5432,7696,5676,0,5655,7930,5476,7628,5726,8365,5651,8447,7487,0,0,0,5261,7658,7715,5485,8411,8164,8554,8179,0,0,0,0,0,8539,8316,7738,5426,0,7888,5237,8316,0,5377,5261,5407,7972,0,7482,0,0,7811,7548,7706,0,0,5661,5661,8115,7994,0,0,7738,8427,8381,0,0,0,5476,8504,5320,5496,5314,0,7746,5325,7565,8456,7663,8010,5708,8196,5280,7675,0,0,8255,0,8316,0,5337,0,8543,5564,5573,7605,0,5573,0,8560,8566,5291,0,5496,8571,8577,5301,7500,0,7628,8370,5301,5730,7817,7990,8467,8407,5296,0,8582,7592,8365,5564,7867,7930,5237,7839,8334,0,5237,5688,5464,0,0,5442,8589,0,8407,8407,8594,8514,8093,8087,7828,8179,5459,7935,5352,0,8170,5325,8277,5606,8467,0,7774,7706,7628,5401,0,0,0,0,7628,5325,7994,5651,8402,5661,7500,5237,5688,5296,0,7628,7867,8598,7805,7888,7957,0,8334,7658,7576,7482,0,7893,7914,8467,5476,7643,7719,7491,8438,5501,7849,0,7482,8604,8447,5708,8527,5401,8227,7877,7538,8539,0,5329,0,5476,5396,0,8422,7864,5396,7962,8447,5401,5301,5274,7834,7610,8514,5407,0,0,8411,7628,8154,7622,5442,8370,0,5333,5381,7994,5476,7811,7849,8179,5347,7482,5661,0,7632,8093,7542,8154,0,8494,0,0,5717,0,5459,5218,5325,8608,8613,5237,8617,7500,5476,7982,5325,5726,5606,0,7542,7715,0,7790,7482,5671,7904,5274,7790,7565,8387,0,8022,7994,5529,0,8625,8484,7864,5209,8402,5358,8631,0,8577,0,0,7908,0,8370,0,7605,5248,5274,8190,0,5688,8456,7952,7696,7817,7723,0,8149,5301,8033,8589,8073,8494,8057,0,7888,0,8594,7738,7972,8473,7786,0,0,5224,7972,7582,7558,7999,8028,7516,8312,5325,8438,7994,7935,8131,0,8613,0,8201,0,8005,8149,7653,7616,7972,5381,5426,5661,8329,5261,7542,5661,7588,8370,5651,7643,0,0,5296,8136,8022,8543,7511,5381,5464,5347,0,7982,8307,8154,5717,5642,8604,7952,5407,5407,5442,5309,7538,5296,8514,8179,7672,7920,7924,8170,8170,5699,0,0,8635,0,8484,5396,5274,5661,0,7588,7893,8604,7491,0,0,0,8196,5296,8068,0,8322,5237,5476,0,8249,8641,5347,8052,7839,8533,8647,7632,5651,0,0,0,5661,8073,5296,7706,5661,0,0,8073,7542,8282,5285,5291,8653,8022,8170,7864,8077,0,5459,5459,0,7952,0,7706,7805,0,7628,8196,0,7990,5564,0,8539,5274,7952,7849,8658,7653,8316,8073,5261,5358,5329,7491,8447,8663,8589,7757,5329,7853,5401,7990,8589,5655,8663,5401,7790,0,7542,7990,8670,8255,5358,0,7482,0,5261,7588,7730,7872,5314,5329,8093,5496,0,7811,0,0,7977,8282,7982,8307,0,7957,0,5676,7990,5612,5717,5578,5693,8676,7605,7834,8334,5496,0,0,0,0,5476,0,0,5337,8234,5642,5642,5391,7817,7706,8682,0,0,5730,5651,5564,7982,8687,7883,0,0,7834,0,0,5726,0,8227,8391,0,8222,0,0,0,5274,0,0,0,7790,7843,7823,8456,8033,0,0,8694,0,7867,5491,0,8653,5459,5329,0,0,0,0,7628,7658,7877,7800,7746,7811,0,8699,7811,5730,5485,8073,5274,5301,5309,0,8211,8647,8052,7924,0,5274,7588,7994,7935,0,5309,8554,7849,0,8635,8093,0,8217,0,7734,8354,8073,0,5296,5459,7834,8170,5285,7908,8577,7628,5377,0,7478,8062,7675,8052,0,0,0,0,5464,5564,0,0,5471,0,8705,5285,0,7528,0,0,8467,5291,5396,0,7898,8711,0,0,7994,0,5712,7487,8411,5329,0,0,0,7849,5617,7672,5213,8164,5688,8577,7817,5274,5237,0,0,5453,0,0,0,5661,5666,0,8498,5314,7893,7571,5459,5325,7849,5325,0,0,0,0,0,5647,5274,7834,5629,7738,8539,5291,8608,7653,5642,8170,7872,7898,0,8077,5606,8716,0,0,5337,7746,0,0,8716,5426,7706,0,8682,8316,7696,7977,8196,7790,7864,5301,7710,0,7849,5381,5325,0,8721,5730,5671,0,5501,7719,0,5325,0,7723,7849,0,5274,5381,5396,5358,0,0,5655,0,0,5391,0,8022,7972,8543,0,0,0,7843,5358,5274,7898,5491,0,0,7920,7877,5358,7588,8154,8727,7616,8732,5419,5362,7786,7565,0,0,7738,8402,0,8407,8297,8739,7935,8245,7982,8402,5329,5676,5329,7542,8174,5655,0,5301,8340,7706,0,0,5377,8093,5726,5401,5329,5320,7817,5578,8473,5309,8745,0,0,8548,7734,5309,8473,5407,8234,0,0,0,8577,5301,7632,7914,7746,5407,8539,7930,0,5401,0,8329,8751,5699,7610,8397,0,5291,0,5617,0,5661,0,5347,8140,5476,7495,5301,8354,0,5476,0,5325,8756,5661,0,5209,7990,5296,5726,0,0,0,8631,0,8329,0,7914,5568,7867,8302,7500,8387,8312,8443,5337,5209,5401,5309,5367,5476,7867,5651,5285,0,7710,5391,0,7482,5305,5476,0,7719,5309,7491,0,5699,7935,8375,0,5241,0,0,0,0,0,0,8402,0,8345,5519,8179,8732,7687,0,5325,8073,0,0,7843,8422,0,8093,7864,8494,5401,8762,8514,5347,7994,8676,7908,7663,5437,5296,5476,5305,5209,8543,5413,0,5358,5301,5329,7632,0,7883,0,5699,8033,0,7558,8484,5568,5325,8093,0,8670,8509,7872,8287,8766,0,7957,8222,0,0,8222,8164,0,5666,0,0,5726,0,0,7972,8772,8164,8443,7616,7790,8716,8411,0,0,5629,7653,5372,8164,0,0,7734,0,8772,0,8297,8381,8577,5459,5459,7548,0,7672,7883,0,7952,5325,8447,5476,8184,7628,7599,8039,8115,7834,8402,5274,5285,5320,5564,8456,0,0,0,0,8201,8411,7710,7730,5476,8411,0,8217,5347,0,7637,0,8190,7628,7628,0,0,0,0,7738,0,0,0,0,8170,5661,5237,5407,0,7533,7738,8190,5274,5453,0,0,0,0,0,5651,5325,8131,7752,0,7599,5337,8115,5358,5651,0,5661,8010,5476,0,5666,7616,0,5309,5617,7482,5325,0,7588,0,0,0,7599,7746,7588,5476,5437,7719,5325,7632,0,7790,7687,5342,7972,8411,5301,0,0,0,0,0,0,0,8255,8721,0,5655,0,8077,7667,5661,5642,5661,0,8766,0,0,7610,8033,5651,5699,0,8136,0,0,0,7588,5661,8073,5693,8498,0,8519,7935,7653,7883,8052,0,0,0,7914,5651,7877,8514,7653,8402,5291,7849,7893,5391,7935,7914,7555,5564,7800,0,0,0,0,5476,5442,0,0,8222,5666,7990,7828,5358,5342,5717,5358,5476,5274,8447,7616,7605,8104,8527,7500,8647,7478,8239,8777,7967,0,8411,5642,7811,5213,5448,0,8217,5485,5237,0,8039,0,0,7834,5529,5573,7946,7616,0,0,0,5401,5274,7811,0,0,5401,5261,8777,7930,5407,5320,8010,7828,0,7904,7628,0,0,7700,5666,5476,5491,0,7877,8783,0,5309,5362,0,0,0,8239,5712,8484,0,7511,7653,0,5218,5309,7710,8504,8789,0,0,8062,5606,0,0,0,0,8073,7723,0,5320,8402,5296,8783,8307,8625,7864,7994,5501,8772,0,8099,7849,5218,5296,0,7628,8548,7834,5688,7999,7592,7658,0,0,0,0,0,0,8422,7516,7592,7687,0,7849,0,7500,0,0,0,8402,8745,5305,0,0,5413,5301,0,0,7834,5717,5305,7930,5676,5459,8670,0,7478,7834,0,5291,0,7658,7653,0,7828,5325,5381,0,0,0,5661,7986,8010,8154,5237,8077,5651,8297,8498,7482,5651,0,5291,0,8316,5509,0,5301,8670,8571,8795,8282,0,7681,0,7930,7491,7858,7877,7982,8800,0,8345,8104,7542,7930,0,5501,0,5480,5480,7571,8093,0,7952,0,5391,5730,8577,0,7487,0,7930,8504,8762,8073,7779,7839,5358,5325,8548,0,7849,5301,8467,8354,0,7877,8539,5666,5309,5248,7487,7734,5476,0,0,7811,8272,8613,7616,7616,7648,7482,5391,7482,5291,8068,8190,0,0,7867,7605,0,0,5337,0,0,5237,5391,5476,5305,0,0,7982,7675,7478,0,5485,8805,5391,8447,0,7738,0,5573,7877,8647,7757,5386,5386,8811,8131,7628,5730,0,0,0,7982,7924,0,5274,0,8164,7957,7999,8179,8239,8647,8370,8010,7653,7883,8427,0,7738,7605,8282,5407,7628,5314,5337,5301,0,0,0,7877,5401,0,0,8484,7734,5688,7817,5209,7628,0,0,8705,8816,5712,0,8484,0,7500,5255,8411,7790,0,0,0,0,5476,5337,0,5255,0,8222,5296,0,0,0,8201,8211,7986,5647,7478,5301,8093,0,5325,7667,0,7888,5291,5491,7930,0,0,0,0,0,7986,8411,5342,5647,5459,0,0,0,0,0,7888,0,0,5342,5358,5647,7719,8179,5647,7746,0,7672,0,0,8427,5358,5325,8433,0,5301,0,0,5342,0,5320,0,0,7864,0,0,0,0,7723,7632,7482,8068,7786,7628,5666,7930,8028,7858,5401,5688,5401,0,0,5301,5693,8631,7576,0,5426,8608,8010,5352,8126,5480,0,7864,8126,7706,5573,7817,8316,7653,7558,7977,7811,5426,7828,8349,7582,7864,8821,8577,8119,7742,8539,7628,8732,8164,0,5629,8827,8647,8617,5329,8316,5241,5301,7730,0,0,0,0,8625,5476,7653,5661,7632,7511,0,7883,0,0,8772,0,0,0,5337,7823,0,8461,7667,7710,5568,5509,5301,7511,8577,5367,0,8345,7628,8170,8110,8329,0,8451,8179,8833,5218,5564,8821,0,5237,5320,5285,5285,7653,8015,0,0,8839,8033,8010,5325,0,0,7786,7478,0,7867,5320,0,5480,8762,7487,8297,7715,5564,8093,5325,5519,8190,8716,0,5381,5301,8245,5325,0,7672,7914,5301,7843,8845,7482,0,8196,7867,7935,5337,8201,5464,5699,8277,0,0,7500,7898,7990,7706,0,8119,0,0,7576,5464,8422,7982,7930,0,0,0,5325,7786,0,5524,7637,7538,7605,8422,8422,5401,7843,0,0,0,5491,7663,5274,0,5237,0,8422,8179,5514,0,5381,5501,8658,0,5642,8479,7977,5666,7834,5329,8422,7616,7920,7632,5301,5432,0,7790,8126,0,0,5642,7849,8104,7982,8381,5442,5301,5629,7811,5358,0,7935,7924,8245,5708,5529,0,0,0,8297,0,5372,5509,7738,8222,0,7834,0,7605,8387,8010,5337,0,8422,8249,5617,5309,7786,7706,8255,5285,7834,0,8795,7746,5274,8589,8136,0,7811,8816,7558,0,5291,5237,7706,8608,5712,7786,0,0,8010,5726,7877,8598,5358,7790,8851,7786,8062,7811,7632,8560,7977,7768,0,8211,8201,7786,0,7715,7663,7616,8598,7843,5381,8539,7977,0,8751,8504,7834,8762,8539,0,7843,5274,0,5426,7930,8345,5651,7706,7706,8494,5617,7628,7977,7558,5642,7616,7924,5325,5381,7653,8438,7957,8509,5642,7834,7962,7952,5464,7616,8119,5496,5401,0,8190,8732,7935,8447,5726,7622,8154,7628,7823,7982,8554,0,7542,5617,5419,5708,8349,8211,8427,8411,5285,5291,8789,5358,5391,5301,5305,8577,7763,5305,5237,7877,5726,5285,8154,0,7500,7700,8316,5730,7628,0,0,8073,8857,0,8287,0,5381,0,5426,5296,5391,0,0,0,5642,7628,7898,5381,5578,0,5617,8539,7582,7839,5564,8438,5655,5655,0,7834,7757,8062,7817,0,7672,8625,7986,8402,8048,8451,7834,0,5381,0,8039,5612,5509,5241,8077,7757,8277,7738,8402,0,8234,0,0,0,0,7746,0,8438,8170,7628,0,7999,8115,0,0,7533,7849,8131,8062,8149,7478,8287,0,5329,8140,5401,0,8255,5320,0,0,8433,0,5296,7588,8519,0,7610,0,5386,8772,7883,7994,8811,8245,5301,0,5274,7533,7986,0,5453,8359,7616,5274,5347,5305,7904,0,5564,5358,0,8154,5476,7511,0,0,5676,8154,7558,8307,7864,0,7834,8863,8762,7706,7628,7768,8154,8077,7946,8170,5617,0,0,5352,8391,8340,7957,5301,8104,0,8402,5261,0,7706,0,8277,8093,8239,8705,7628,5386,8287,7706,5476,8184,0,5407,8387,0,7805,0,0,0,5358,7883,8312,8184,7972,8783,5606,5606,8110,5476,8577,8345,7849,7723,5301,0,5209,7588,8467,8387,8762,5509,0,7582,5337,5296,5407,7872,8211,8170,0,0,8345,5342,8277,7643,8322,0,7888,0,8136,8179,5377,7500,8484,5464,7706,5381,7790,7834,0,7972,8422,0,7663,8334,8777,5629,8869,7643,7706,5573,5612,5314,8417,5647,8443,7957,7622,5301,7800,7786,8433,5396,5712,0,5401,8479,5261,7839,0,0,7972,7904,8402,5688,0,7610,0,7893,8039,7558,8484,7864,0,5291,8062,5285,8498,5642,8316,5496,5337,5320,5642,5717,7811,7843,5291,8033,0,0,7914,0,8391,8391,0,8519,8267,0,5309,8287,0,8772,0,7723,0,7558,0,7628,7558,8316,0,8255,0,8170,5347,5476,8164,0,0,7487,0,8048,0,8249,7952,0,0,8370,8255,8287,5699,8217,5301,7904,5381,0,7632,0,8267,0,7982,7482,0,7972,5401,0,8811,5407,7576,7605,7692,8136,8217,8115,7924,7605,7817,5476,0,0,8519,8149,8504,8164,0,8136,8604,0,0,5476,5291,5358,7558,7667,5442,5296,7710,7972,7834,0,0,8099,5224,7999,7653,7628,5491,7999,7893,8302,8093,7738,0,8211,7491,8222,8005,7795,7823,5717,0,7710,0,5612,7839,7576,7658,7908,8196,0,8762,8604,7811,0,8159,5241,7542,0,0,7994,0,0,5237,8136,7734,8359,8170,0,0,0,7533,7738,7542,0,8164,7632,7982,5448,0,7734,7763,0,8851,8349,5320,8093,5666,5419,8873,7972,7757,0,8589,7982,8010,5712,5342,5407,5629,5661,0,5305,8010,5666,0,8554,8093,0,8827,8267,8249,7628,7972,7706,7883,8154,7994,8772,7719,5661,5448,8022,0,7511,5476,0,5666,5301,7893,8577,7516,5274,5642,0,5432,5274,8261,5622,0,7883,8411,8093,8062,7706,8239,8282,5651,7982,7982,0,8604,8456,7723,8391,8484,8201,5712,8589,5712,0,0,5301,7768,0,8869,7521,0,5391,0,0,5347,5464,8484,7834,0,7730,8641,5661,5578,8514,5237,7800,0,5464,8772,7675,8387,5358,7632,8010,5301,8402,5496,5442,8539,8582,5573,0,7877,5651,7542,5688,5655,0,0,0,7786,7834,7710,8625,7715,8073,7473,8845,5642,7790,7599,0,0,0,8613,5291,0,8438,5419,0,8427,7571,8354,5224,0,7576,5358,8504,7628,0,8287,7696,5476,5651,8745,0,7599,5325,8201,0,5432,8010,5642,5301,8010,5726,5564,7696,0,0,0,8154,5309,7706,8170,8277,7935,7930,8816,5476,7500,7511,8329,5564,0,8566,8805,5413,7687,0,0,0,7742,8851,8845,5629,7952,5296,7738,0,5642,0,7706,7653,7672,5651,5651,0,5329,8010,7742,5651,5381,5564,5651,5464,7588,5642,8077,5309,5401,5358,8745,5377,8093,8857,7972,7482,8479,5442,7774,5651,7834,5347,5248,0,0,8721,5617,7632,0,0,7632,7605,0,5442,5676,7592,7696,5476,7872,7774,8504,5564,5347,5342,7628,5642,7828,8093,7605,7738,5255,7542,5291,7805,8201,7786,8879,0,7653,7653,8711,7982,8062,8539,5564,8190,5651,5712,7843,7786,7999,7738,7628,8548,5301,8239,7478,5666,5325,5666,5301,0,8073,8608,8005,5480,0,0,7999,7952,0,7500,8716,8287,7888,0,7582,7952,8073,0,7811,8190,8179,5301,8422,5464,5529,7982,8222,8297,7681,5564,8239,0,5476,5320,8312,7849,8140,0,8136,8345,8783,5329,8427,7738,0,0,0,8255,7588,0,8721,0,0,5573,0,7696,7696,8699,7706,7487,5573,0,7834,7898,8345,0,8884,8519,7904,0,5377,7653,7571,7904,5578,8631,8119,5358,7696,7605,7478,7752,7972,7576,5329,7752,8234,5688,7986,5437,5448,7746,7972,7706,8340,8647,5401,0,7610,7588,7706,8467,8527,0,5291,8539,7858,8721,8816,8504,5655,5671,7920,8772,8783,8104,0,8104,5305,5432,7706,5391,5358,5291,7487,0,5396,8136,5296,0,7696,0,8711,0,5237,7972,8504,8170,5459,5578,8670,8249,5358,7653,7628,5274,0,8766,8179,8201,0,7706,5325,5453,5329,7592,7653,5476,7774,8104,7972,5209,8245,0,5606,7576,0,8104,0,5578,0,8110,8888,5325,5476,8762,5291,5717,5325,0,0,5612,8893,7521,5464,7706,7605,8451,7710,5534,0,8898,7786,8509,8010,0,8340,7972,7877,5666,0,0,5578,7628,7952,5464,5309,0,8039,5514,8772,0,5237,7800,7888,5708,7920,7478,7663,8249,7920,8249,7924,7972,8073,7616,7994,5386,5347,7834,7877,7768,5655,7734,0,7658,7893,5320,0,8903,5496,8297,5606,7710,7738,7977,8598,5464,8239,0,0,0,5309,7864,0,5564,7888,0,7864,0,5642,8539,0,8539,8201,7800,7935,8099,7706,7715,8179,0,0,0,0,5337,0,8245,0,7588,5564,5325,5419,8340,8073,8909,7935,5564,8201,7478,0,7924,8391,8334,8211,0,8249,8340,7681,8509,8126,7675,7528,7706,0,0,8670,7946,8816,7675,8033,0,8010,7482,8608,8297,8915,8048,8010,5726,5391,7849,8411,5491,5305,8919,0,0,0,0,5496,7888,5337,8052,0,0,5274,5666,5309,7706,0,5712,0,0,0,0,7834,5476,7482,7706,5578,5237,5476,7491,7864,0,7972,7653,8287,0,8104,5224,7908,5442,0,5480,7723,0,8484,0,8140,5564,0,8249,5301,8577,7738,0,7786,7779,0,7986,7828,7839,0,0,0,5325,0,5496,7790,8762,0,7637,7872,8727,8821,0,8277,5426,0,5476,5712,0,7849,5325,0,7653,0,5301,5209,8443,5642,0,5301,7706,8811,5301,0,0,0,0,7588,5442,0,5661,7930,7500,5485,7734,8093,7521,7571,7757,8104,8745,7823,0,7672,7710,8670,7628,5396,5301,0,0,0,5301,0,0,8217,0,5476,8381,0,7982,5564,0,7558,7823,8604,0,5377,8196,8473,5342,7994,0,7719,7663,7864,5381,8739,7893,0,7482,0,5514,8307,5296,5661,8115,8772,8227,7972,5476,5564,7864,0,7667,0,5496,5274,7800,0,5358,8093,5381,8005,7977,0,0,7843,0,7893,5329,7687,7849,0,8447,7616,7675,8909,8149,8115,8005,0,7542,8170,8647,8282,8307,7516,5651,5314,5661,7982,5651,0,8925,8827,5717,7823,7605,7834,7653,0,5476,8375,8354,8297,8093,8929,7628,7555,0,5476,7555,8196,7542,5367,5573,5309,5651,0,8136,7653,8443,5367,7834,5647,0,0,5612,7482,5248,7576,8211,8467,7555,7622,5514,5509,5301,0,8136,5391,5358,7839,7898,0,5676,7521,7952,5213,0,8329,7687,5642,0,0,0,7864,7924,7681,5337,5291,5301,0,5301,0,5325,7982,8427,5564,8227,8721,8170,8249,0,7558,8711,0,5301,0,0,0,5476,5224,0,5730,5509,7710,0,5717,7653,8073,0,7511,8179,8179,5476,8190,7834,5612,5524,5218,0,0,0,7628,0,5578,7538,0,0,8617,5309,8721,7734,7516,0,8805,0,7986,5476,0,7839,7834,5381,0,7994,5274,8381,5564,5717,0,5305,5564,8716,5274,7752,5301,8179,7763,0,8447,7637,0,7500,7555,8349,0,8795,0,8560,0,0,7628,7994,7605,0,0,5476,0,0,5564,5358,7605,5209,5320,5367,8447,0,5285,8190,0,8417,8164,5459,7730,0,0,7628,0,0,0,5329,7653,0,5476,8397,8427,7588,5509,8447,8721,5291,5413,8179,0,7888,7706,5642,7952,8699,5730,8329,5661,0,5296,0,5661,8073,7990,0,7500,8126,0,5573,0,5377,5573,7710,5453,8721,8527,5661,0,7888,5291,5296,8833,7730,7888,8093,8249,0,8039,8249,7610,0,7616,0,8721,8140,5301,8762,0,7864,0,0,0,8407,8407,5419,5401,0,8670,5237,0,7994,0,0,7849,5337,5329,8277,7742,8119,5676,7706,0,8267,0,7706,0,8873,5325,8349,8519,8184,0,5301,8093,0,8033,0,5274,7982,0,8443,5491,5491,7643,0,7935,5377,0,7495,7972,7511,7893,5285,0,5708,7516,0,8705,8272,7920,7528,5261,5661,8631,8272,0,5237,5661,7920,5642,5642,7555,8052,8566,7482,8010,7823,7710,7599,8467,5367,7576,8467,5391,5651,0,5717,8174,7893,7982,8447,7839,5651,8411,0,8136,7628,7864,7972,8566,7500,7672,5296,8329,5386,7653,7977,8307,5396,0,0,8140,5237,8022,5651,7528,7478,7723,5241,5442,5426,8484,8170,7643,5377,7800,5407,7706,8087,5666,5509,7935,8179,5381,5337,5296,7687,7588,5717,5386,5237,0,0,7710,7994,5305,8234,0,0,5372,5296,8375,8239,8149,0,0,5280,5708,8077,5342,8349,8772,7528,5485,8345,7738,7877,7994,8641,5325,8033,7935,7500,5274,7528,0,0,0,5386,0,7834,5688,8062,8022,7667,8239,8170,0,7800,8577,0,5573,7710,7521,0,5712,5320,8604,7710,7786,7628,0,0,8239,8149,5320,5578,0,0,0,0,7864,0,8302,8548,5291,7667,0,7957,7994,0,7605,7986,0,5578,7999,0,5476,0,8447,0,0,5476,8052,8387,7811,7800,7800,5726,8149,8052,0,8514,5274,5655,5320,7571,7605,8131,8190,5476,5377,0,5309,8179,0,8201,7779,7972,5325,5509,8721,0,8745,7663,5564,7952,8131,5274,8821,7972,8473,5476,5612,5459,7952,8119,8329,8033,5617,8745,0,0,8104,8062,7582,7558,7994,5459,5320,0,5325,8925,7706,0,5564,5708,5564,8312,0,8772,7687,7935,5726,5337,7849,5730,7952,7864,5419,8267,7853,5688,0,5564,7571,7982,0,8073,0,5291,8539,5291,5564,5464,0,5218,7805,5612,7710,0,0,0,7800,8936,7706,5407,5358,0,7972,7605,5726,8427,5325,7828,0,5301,5301,7986,7768,0,5564,5496,5629,5391,8560,7867,7706,8073,8136,7853,5573,8140,8354,8539,8033,5573,5358,8211,7904,5606,8438,8577,8527,0,5688,7681,5564,8144,5407,0,0,0,5655,5688,8451,7710,8005,8577,7898,8762,7478,0,5291,5329,8227,0,5573,7779,5606,8653,8227,5291,5647,5396,7893,5358,0,5280,8494,5325,5726,8005,7982,8140,5342,0,0,5688,7920,7930,8631,5285,8164,0,8140,5459,7710,8451,8249,8190,7888,5432,5578,5573,7877,7952,8676,7967,8919,8277,8909,8354,7768,5320,8093,0,8196,5564,5617,7681,0,7663,0,7723,7757,8370,5261,8082,8427,7610,8484,5301,0,8438,8164,8354,5291,8756,8010,7706,8062,5564,5329,8062,5480,8438,5606,5676,7706,5241,5485,7817,5606,7843,7790,7482,5329,8267,8170,7920,0,5661,8411,0,0,8164,8940,7478,5496,7786,7877,5352,7800,0,0,8617,8946,5622,7877,0,8519,5342,7930,8509,0,7972,7994,8438,8093,0,8028,8093,7706,8354,0,5476,5237,8033,0,8164,8033,8789,5337,5325,8073,7565,0,0,8140,8539,0,7628,5717,7500,7672,7849,0,8126,7817,7592,0,8387,8293,5337,8022,5661,8777,5642,5320,7834,8952,8239,0,7628,8073,5342,0,7834,5666,7516,7658,8762,8354,5325,0,8267,0,0,0,8170,0,8033,0,7521,5274,0,7952,7628,5666,0,5647,8033,5426,5688,8560,0,8903,7774,8539,7834,0,0,0,7877,0,5301,5612,7538,7538,0,5629,8456,7957,0,8451,5647,7864,8179,5381,5377,5325,8903,5285,5358,5666,5476,8104,8104,8312,5301,5437,7658,8312,5606,8170,7500,8010,0,7500,7696,8154,0,8297,8048,0,0,7779,5377,5651,0,0,8433,5712,7482,7746,5647,8005,0,0,5651,8164,8039,0,5651,8170,7920,5309,7599,5325,8249,5407,5529,8222,8811,7877,5301,5301,5688,5471,7616,8267,8033,5325,8297,0,7672,0,7952,8354,5296,7920,5496,7888,7877,0,8739,5617,5426,5534,5391,7710,7491,7653,7893,7491,7834,8857,0,7482,7888,5274,0,0,7867,7877,7653,8249,8354,8140,7542,5309,5261,7715,0,0,0,5241,7972,8104,7628,8711,5509,5442,0,7588,8959,5606,8739,8140,5342,0,5381,5407,5708,5285,7491,0,7491,7610,5372,8222,5401,0,7811,7672,7994,5274,8964,8073,5241,7605,7904,8249,5661,0,0,0,7675,5301,7687,5651,0,7491,0,0,0,5661,0,0,0,5261,0,0,5261,5291,5325,7565,0,5453,5708,5573,7478,8721,5661,8805,5661,8903,0,8772,0,5367,7500,5358,7487,8154,5241,7478,0,8489,8663,5391,7811,8144,5564,8663,5274,8196,7867,5347,0,8387,0,5699,0,8104,7972,0,8005,7542,8560,5476,8447,5241,8663,0,5391,0,7839,5291,7500,8267,0,0,5573,8479,7828,5717,5377,8062,8447,0,5301,0,5274,0,5237,8190,0,0,0,8277,0,7715,7558,5407,5358,7706,8093,8052,7898,5629,8539,5564,5274,8969,0,0,0,7734,5296,7994,5476,7491,0,7935,8052,0,0,5274,7877,7643,0,7653,7839,0,7706,8005,8110,0,0,7504,0,7982,8402,0,5699,8539,7935,0,0,7834,8239,5358,0,0,0,0,7555,8222,7616,8062,7839,5280,5377,7628,5666,8222,0,5325,5564,5274,8433,0,7719,0,0,7628,0,7663,7653,7883,5717,8539,7982,8334,0,0,8093,8349,8349,5401,0,5529,5655,8312,5564,8417,5237,8217,7883,8267,5274,7482,0,8217,5413,7982,7893,7893,7542,7930,0,0,5391,5606,8427,5407,0,5296,5564,5509,5305,8255,8239,0,7763,5717,8062,7628,7706,7888,8608,0,7904,8539,8190,8539,5358,7576,8174,8184,5476,0,7977,8245,7888,5237,7883,7681,5661,8227,8467,8721,7779,5413,0,5329,5325,8190,5320,0,5320,7982,8267,7877,0,8164,7864,0,7904,8527,0,5578,5501,8721,8201,5717,0,5464,8762,5377,8073,7542,7986,7610,7994,8427,5347,0,8136,5358,5480,5285,0,8201,8170,8312,5367,7994,5564,7849,8010,7877,7982,8925,0,0,0,0,0,8010,5459,8062,7710,8222,7482,0,0,8073,8821,5442,7616,0,5671,0,8267,5655,8387,0,5730,8438,8456,5717,8411,0,8052,5651,8119,8682,5358,7888,8104,8976,8589,7558,8297,8982,7790,8073,8422,7920,5564,0,0,7516,0,8514,7478,7706,7972,8033,5367,7849,8940,0,5730,0,5564,7648,7643,8456,7542,0,5237,8391,8005,5717,0,5708,8589,8617,7957,5655,5296,8179,0,7516,8136,7653,8762,5309,7746,8010,7864,5386,8062,8062,5529,5529,0,5358,0,5337,5476,5529,7730,7628,5301,5301,8467,8217,8052,7867,8873,0,0,8039,8201,7994,7994,0,8345,7500,8987,7946,7924,0,5642,0,5358,0,7946,8272,7511,0,7605,0,7706,8783,5476,8307,8467,8140,0,8073,8179,7924,0,5476,8184,8670,8543,0,8282,5305,7663,8869,5617,0,7972,7924,5407,8467,8217,5377,7994,8329,7972,5476,8504,5391,7982,8884,5476,5407,5342,7616,8222,7982,7982,5661,0,7924,5401,0,8869,0,5407,7605,7616,7834,5309,8519,7883,0,7908,5476,7491,5407,8869,8381,7839,7542,5529,8073,5651,8839,5651,7779,7706,7706,8087,5358,8073,5413,5432,8447,7994,7811,8010,7706,7511,5564,8608,8608,0,7706,8658,5480,5476,8073,7858,8073,0,8190,0,0,7628,7710,5329,5476,5301,0,5453,5237,7478,0,5666,0,7999,8397,8287,0,8345,7696,7628,8873,5564,8302,7811,5573,5448,0,5358,5459,0,8539,5301,0,7653,5437,7786,7558,7957,5305,5651,8349,5708,7883,7605,5464,8010,8919,8316,0,8762,7982,7994,5325,7710,7516,0,8334,5407,8077,7632,5325,0,0,8077,5476,0,5509,8387,5280,8073,0,8093,8811,8154,8370,0,7946,8005,8433,0,7843,5358,7811,8312,7742,5407,5377,5342,0,8217,5708,5407,5305,7952,7908,8345,7538,8745,5501,0,8898,8052,7888,0,8716,0,5381,5564,8994,0,8227,8077,7800,0,7839,0,8144,0,8539,5564,7681,8443,8494,7893,7774,5352,5391,0,8110,7706,5496,7533,8073,7800,8548,7839,7952,5325,5347,0,8249,7746,7533,5329,8498,0,7511,5476,7752,5301,5301,8833,7605,7643,7914,7710,5426,7599,0,8154,7920,0,5712,7935,5712,8783,8170,8417,0,8447,8613,8093,8387,0,7706,0,0,7898,0,8115,5381,7628,7528,7930,7904,5480,0,8255,5337,8539,5381,5305,8119,5261,7706,5261,5651,7883,5476,8845,8845,7853,0,5529,5476,8805,7681,8711,7511,0,5301,0,5329,7500,8451,7904,7511,8170,8427,8170,0,8443,5274,7972,0,0,7972,0,0,7986,0,8845,7864,7715,7548,8539,7834,0,8845,0,7843,7628,0,8136,8370,5396,8427,0,7957,7877,5459,7994,8190,5372,8438,0,0,0,0,0,5480,8433,8739,8909,8028,8149,5329,5476,5661,7834,8745,7734,5442,5274,8721,5381,8277,8249,7491,0,5301,7482,0,0,8909,7628,0,0,5496,8762,8062,0,5237,0,7706,0,8467,5325,8267,7565,0,8560,5655,7982,0,0,5381,8456,8811,8190,0,8479,8255,7839,5459,7904,7681,5509,7839,7972,8126,7478,8222,7883,7904,8316,0,0,8484,7952,7839,5337,8467,8149,8484,8998,8028,7864,7864,5426,7849,7834,5647,8201,5413,5568,7805,0,7706,8467,0,0,0,5730,5464,0,5676,8005,7864,5325,5305,5248,8222,7478,5655,7500,8245,7710,0,8827,7538,5568,7834,8179,0,7849,0,5347,8316,7628,8560,5386,5301,5501,7734,8093,5708,8190,5476,8190,8249,0,5666,7834,8222,5459,8093,5391,5285,8577,5485,8170,5377,7920,8461,5285,8359,8201,8539,7811,5642,5301,5651,0,0,0,8504,0,0,8647,5329,8093,5476,7774,5261,0,5320,5480,5305,7849,8964,5224,5352,7643,0,8039,8201,5301,7877,8126,8451,8249,5612,8322,7972,8582,5358,5708,8249,5274,0,7853,0,7994,0,0,0,8149,7877,8447,8447,5301,7582,8170,5407,5564,8302,7687,8272,5407,8789,8387,5485,8811,8539,5407,5712,8245,5407,5617,5661,5437,8211,5301,8234,7628,7738,5578,7924,8641,0,7849,7757,7994,8267,7687,5726,0,0,0,8805,7786,0,8936,5564,8964,7738,7738,5314,7653,5255,7849,0,7576,8073,7511,7555,0,8174,8539,8302,5377,7908,5377,7706,5325,7710,8994,5309,7990,5301,8170,5726,7986,7786,5280,7706,5274,5407,5391,5274,7663,5396,8170,8115,5642,0,5325,5496,8795,0,0,7834,0,0,8316,7823,8354,0,7710,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5476,5693,8898,7706,8987,5367,0,5305,8015,8589,8987,5367,5325,8201,7628,7746,5671,5442,5442,5622,7867,7920,5391,5391,5688,5224,7491,8827,7582,7487,7558,9003,5688,7779,9003,5337,7779,7834,5224,8940,8234,8484,5717,8387,8322,7548,5325,5381,8527,8721,8732,7877,7516,5314,7605,0,5509,7883,7994,7605,5485,5301,8170,8959,5564,5476,5476,8716,0,8484,8484,5578,5642,5476,0,7972,7478,7687,7982,8048,5333,7548,5255,8334,8255,7982,8625,7558,7558,5476,7482,0,7763,8484,7706,7491,5237,7521,5671,8484,0,0,5320,0,5391,5325,7700,7977,0,5391,0,0,8349,8375,0,8613,5529,7834,5564,8302,8136,8811,0,0,0,0,8811,7710,7952,8811,8375,7516,5476,5291,5333,0,7628,5241,5391,5391,7696,5573,7628,7628,7628,8267,7982,7706,7478,5485,7487,5342,8682,8682,8387,5661,8387,7706,8387,5325,7853,7599,8756,7738,5285,8370,5476,7877,7946,7715,0,7521,5485,8805,7779,7521,5485,7491,8196,8073,5337,5661,7883,5401,7888,7994,8504,8504,5325,0,7599,5476,5391,8504,5333,5693,8115,5309,7734,5442,7904,8015,5309,5381,5224,8705,8827,9008,8589,8099,0,8777,5209,5391,7738,8015,5564,5274,5274,5480,8062,8316,8504,7542,8811,5476,8539,5491,8093,5476,8783,8227,7730,8987,5476,7715,8514,8504,7977,8783,7924,7542,7548,7920,5509,7478,5407,5381,5642,8527,8777,8504,7599,5501,8539,7565,7548,7706,8456,7628,7757,5471,7752,5476,7528,8756,5476,5325,7663,7478,5325,5564,8048,5519,5309,7930,7883,5564,5629,7548,5337,8154,5296,7628,8402,8732,8589,7994,8732,7935,8387,5274,8987,8227,8099,8777,8329,7528,7994,7605,7710,8190,8293,8073,7786,8447,8447,5661,8282,7500,8093,7576,7914,5726,7487,5413,7487,7977,7920,5476,7487,7663,5241,7828,7653,8349,7883,7605,8201,5726,5358,8670,7839,7994,8539,7605,7571,5337,7924,5377,8427,7849,5358,5480,5617,7628,5476,9008,7571,5568,8381,8316,8447,8190,5726,5218,8631,7542,0,9012,0,8456,8359,7849,8149,7521,5717,7924,8126,8048,8391,7977,7696,5224,5325,8727,8154,7952,7768,7681,5285,7839,5476,5391,5426,7914,5480,8795,7972,5671,8277,5476,7710,7473,8687,5301,7482,5391,5651,7605,7500,5337,8345,5329,8711,7696,8099,8316,5496,7710,8329,7817,7576,5476,8613,5407,5367,7977,5437,5329,0,5285,5655,8604,8255,7908,7637,8381,8789,5309,7478,5730,7592,7558,5655,7571,5442,8196,5329,8170,5320,8805,7914,5480,8073,7823,8052,8411,8422,7628,5237,8140,7864,5688,7500,8582,7692,7478,7667,5476,5564,7843,8443,8255,5301,5218,7643,5485,5274,8589,5213,7588,5564,5237,5301,5564,5255,8222,7628,7487,5367,7528,5519,7628,5377,7935,5442,0,7834,9016,5301,8277,7487,5407,5519,5564,7849,8170,7675,5213,5407,8473,5573,7893,8527,8387,8345,7930,7628,7528,5358,5407,8227,7710,5224,8365,5642,8402,9016,5509,8277,8777,8548,5717,8721,7930,5606,5320,5320,8514,8479,8772,5291,7805,5426,5352,5629,5730,5476,7706,5377,8811,5485,5381,5342,8227,5381,7565,7972,8571,5676,8548,8170,7542,8447,8099,5314,7994,7632,8022,8873,7643,7582,5209,8543,7478,8170,8716,7774,5407,8533,7491,7972,8302,8745,0,7616,7558,5407,7834,8699,7548,8104,7977,5381,7994,7628,5224,8893,5377,7994,7858,7834,8527,5629,8494,7864,5661,7935,8119,8795,8539,7571,5496,8805,7930,7491,5464,5642,8427,5442,7834,5337,5407,7500,7687,7839,5564,5358,7972,8548,7746,5325,5476,5661,8329,7487,8676,8604,5655,7742,7853,7853,7930,8925,5329,5381,7994,5501,8227,7952,5448,7972,7588,5367,5564,8467,8164,5301,8613,8267,8131,7538,7898,7828,5629,8052,5381,5237,5261,7542,5291,8777,5573,8589,7930,7675,5218,5642,8772,5612,7924,8048,5224,7977,5491,5437,5629,0,8888,5573,7495,8179,5666,8402,5218,5372,8548,5352,8022,5708,8732,7628,8533,8694,8987,8322,5485,7710,8302,7653,8015,8745,7834,8539,5237,5329,7930,8647,5309,5401,5329,7800,7542,5274,8772,7616,7582,5622,7521,5261,8255,7592,8422,9021,7734,5237,8149,8227,8687,8239,7706,8919,7877,7994,5325,8010,5296,8732,5407,7883,5285,8484,9026,5501,5274,5291,7864,5285,5407,8316,7516,8073,7786,0,7588,5274,5419,8438,7491,5309,7977,8255,7920,8527,7576,7663,8159,5476,7667,8789,7877,5476,8903,5485,8190,7908,5407,5573,7883,5285,8302,5437,0,8888,8345,8613,7658,5442,7637,5347,8447,5309,7542,8249,7478,8381,7723,8447,8227,8577,5666,5329,8582,8222,8762,8473,8365,7972,7487,5337,8052,8033,8033,5301,8909,7994,7610,5573,7877,8261,5320,5296,5301,7632,8365,7495,5629,7834,5666,7700,7696,7478,8267,8365,8467,7504,5248,7999,7504,5274,8687,8217,7990,8217,7746,5647,8227,7696,8217,7696,7528,5647,7487,7834,7834,5391,5391,8903,7542,8307,5501,7790,7914,8307,8126,5485,5301,5464,8613,7599,8028,7768,7779,7628,7904,7817,7511,5237,0,5347,7817,5606,8721,5426,5224,7706,8170,7828,5209,0,7924,5407,8087,5325,5476,5325,7877,7592,0,8297,8028,8467,7790,8461,8297,8604,7592,8604,5437,7795,7592,7790,7576,8711,8131,7500,7746,5651,8164,7482,5661,8795,7667,8789,5476,5651,5391,8721,8249,7548,8170,8467,8391,5568,0,5564,5661,5651,5224,7582,7658,8461,8527,5224,7982,5248,5291,7982,8582,7491,8873,7533,5564,8438,7582,5274,8577,5367,5407,5485,5320,5712,5647,7706,7786,9032,8022,5485,7779,8745,7757,8093,7582,8811,5407,7482,8694,5476,5301,8777,8391,9038,8402,7487,7888,5309,7516,7516,8381,5237,8762,5325,8010,8010,8010,8527,8170,5573,0,8527,0,0,5564,8527,5255,8527,8509,5464,8509,8811,8919,5255,5726,7504,7908,5647,5708,5573,5325,8903,7653,7653,5372,8479,7757,8479,8504,8504,8519,5501,7867,5476,5325,8795,7542,8879,5573,7752,5325,7883,5426,5396,5509,5291,5301,8354,8577,7511,8115,8461,8519,5564,5573,5726,8170,5261,8438,8093,7548,7533,5432,5381,5381,8222,5301,5261,5432,7877,5485,7795,8682,8479,8519,8005,7692,7888,7696,7972,9045,7768,5496,7839,7990,5381,7952,7839,7952,8509,7888,5325,5377,5485,7957,8682,8115,8467,8451,5529,7990,7986,7542,5496,8519,7795,5407,8451,5301,5377,7893,8057,7877,7706,8519,5209,8461,5509,8073,5325,7888,8745,7834,5314,5407,5325,8073,7834,8170,8154,8119,5407,7888,8676,7478,7795,5564,8119,5407,5717,7687,8509,8519,5676,7877,8641,5329,5301,8998,5261,8647,5568,5285,7795,8255,5291,5396,7687,5314,5401,8170,7920,7511,7952,5285,7952,5407,5407,5629,8154,5325,5407,5476,5407,7972,7877,8498,5305,5301,7605,5301,8387,7768,8282,7817,7977,7478,8349,8577,5309,8946,5661,7972,7582,7605,8293,5381,5726,8670,5476,7605,7622,5301,5485,8149,8149,5676,5568,7924,8631,7675,8711,8582,5381,8577,7849,5391,7990,5485,8052,8073,8438,7710,7843,5218,5218,7746,7990,8329,5676,8745,5329,8057,8249,8277,5333,8509,7746,7582,7500,7500,8795,7478,5476,8329,5534,8119,8149,7715,5329,8647,5391,5325,8196,8322,5285,7768,7924,5476,5291,8010,5676,8316,8170,7883,5661,8349,8329,8329,5329,5301,7935,8261,7768,8222,8670,8267,8484,8222,8131,7478,8267,0,0,5671,5671,8222,8827,0,7504,5352,8170,8272,5717,7734,8345,8174,5564,7817,8245,8484,7723,5358,5396,7930,5386,7710,7565,8484,5476,8925,5419,8756,5419,8582,7982,5367,5726,8411,7952,7867,8205,8582,7893,7982,7853,7952,7952,5296,7883,7952,5401,5401,7940,8762,8484,5726,5237,7920,5726,5476,5280,7834,8411,8447,5726,5391,7528,7542,8184,7730,8443,8119,7478,5296,8282,5391,5237,5509,5396,5661,5509,7930,8443,5314,5661,7883,8349,7823,7883,5325,8073,8952,8201,8582,8411,8201,8427,5280,5329,5337,5329,5237,7795,8494,5661,5381,5381,7994,8608,7723,7972,7790,7482,7994,8670,5325,5699,8375,5391,5676,5661,7500,5309,5699,7478,5325,7864,8745,5237,0,7653,8427,7864,8805,5301,5578,7653,5301,8387,8387,7834,5617,7972,7687,8334,0,7849,8387,7972,8604,8334,5377,5301,5617,5730,8805,8397,8451,7893,8467,8604,8467,7521,7834,5301,5291,8302,7828,5573,8484,8196,5708,7952,7834,7487,5301,8227,5347,5301,7834,7834,8467,7487,7817,7516,5688,5726,5726,0,5666,8762,5274,5622,8589,8179,8179,8179,8179,7663,5491,5337,5496,5325,5337,8617,8010,8087,8625,7904,8048,8893,5391,5320,5314,5726,8087,8411,7500,7977,7533,5448,5524,7994,5448,5459,7872,8827,5568,7786,8170,8093,7795,8272,5568,7972,5285,7999,7904,5693,7990,7658,7710,5391,5337,5688,8312,5661,8052,7487,8772,7710,5333,7757,5325,0,8062,7763,7500,8211,7710,5717,8190,5642,5209,7849,5325,7757,8879,8316,8391,5730,5301,5647,7521,7687,5367,7491,5329,9051,7538,7742,5529,5476,7667,5442,7516,8068,7482,7893,5358,8196,8196,5381,8201,8998,7904,5471,5501,7864,7653,8805,8560,8598,7706,8062,7972,7914,7478,7478,8783,8884,5529,5237,7805,8307,7752,5730,7742,7478,5524,8387,7667,5329,7864,40,5712,7663,7706,7558,7478,5524,5564,915,2998,7710,8119,8884,8068,5564,5476,5491,7742,5301,8126,8467,7908,3123,8322,7478,1325,3137,7768,9054,7752,8447,9059,8093,8297,5337,8539,7576,3194,7576,8964,5237,2957,1400,5255,5209,7786,5209,7994,7500,7849,5386,5367,8456,2806,7675,7914,7982,8391,8245,5726,8170,9062,8925,855,7888,670,8174,5358,5564,819,2654,7687,7849,8370,8484,5218,5564,5699,7576,50,9012,7786,9012,7571,8201,7610,7582,8381,0,7487,5274,8154,7706,7696,7864,8687,7687,5237,5476,8577,5209,7763,5651,7972,5642,5325,7576,5717,8952,8365,7675,7990,5296,826,8359,8272,5274,2675,8190,5519,8082,7834,8149,5476,7663,8751,7914,8316,8062,7504,7710,8631,5201,5437,842,2950,5213,7521,7663,8727,8582,8387,8245,7675,5358,8354,2792,5396,1620,7908,7888,2537,5325,8789,8711,9067,8052,8467,7610,855,8211,5717,7864,7710,8670,5301,8136,8443,2665,7692,7675,852,1783,8073,5578,7687,2816,836,3009,5333,5342,2830,8201,7653,7946,8845,8851,5717,5337,7834,7786,7786,8126,7653,5612,8104,5476,7663,7977,5647,5274,36,5301,5237,5629,7571,5688,124,7478,121,8387,8307,28,60,8196,7538,32,4535,905,1518,8149,139,5612,5372,7811,5030,8302,5325,64,371,8527,106,7982,7706,8087,7994,9045,8227,7696,5301,8322,8345,5381,9067,8184,7628,2381,8277,8302,162,5480,7982,7663,7576,26,8800,5314,5314,8987,8539,7946,5347,9051,7675,7521,8068,7478,7752,8402,8302,7982,7706,32,8272,7883,7883,852,1775,109,1318,8287,7849,8387,7706,670,8456,7779,5274,7920,8302,7986,8115,2409,2418,8994,2440,7719,5622,8077,2468,2475,2213,2216,7768,2229,8073,5693,7843,7675,2261,2265,7710,5377,8345,8484,7883,7990,5218,7994,7823,5261,762,56,7986,5381,5730,7653,5459,7576,7637,8082,9071,7628,7482,8365,5642,8494,7746,7982,8267,8732,5296,7811,7811,5237,7849,5325,8005,2792,7994,8302,7500,5464,8762,8077,8190,7779,7628,8940,7710,8255,7834,7883,7440,8179,5301,9067,7687,54,7622,7616,9054,0,8005,5237,7628,5381,7957,7692,5514,8154,5377,5642,7849,9051,5519,8322,7719,5218,7588,8062,5661,5476,5612,7538,5496,8370,7990,7738,8940,5606,8613,5448,8631,5325,8334,7478,5274,5391,5717,5209,5381,7632,5534,8631,7487,7710,8052,5612,7977,8370,5612,8196,8077,7849,0,8159,8044,5717,8136,9075,7663,0,8316,5358,5708,5708,5401,8402,5578,5305,5448,5448,8772,8539,7706,5285,5248,5285,8509,8772,8589,8340,7977,7663,7675,8647,7957,5205,7786,7675,7982,5296,8039,7742,5274,8115,8227,7500,8312,8407,7538,7538,7982,5666,8762,5205,5629,7491,5413,7616,0,5285,5651,7675,5396,8349,8297,7571,5352,8149,7675,5325,7605,8783,8005,8397,7675,7487,8052,5564,8239,8255,5237,5519,8772,8772,5305,8329,7864,9021,8010,7710,5296,8365,8519,8140,7883,7628,8068,7558,5285,7491,5237,7706,8052,5401,7500,8919,5296,7482,7734,5291,8196,8329,8077,5329,7487,7834,7616,8245,7883,7628,7576,5309,8789,8354,5296,7706,7864,5372,5476,5391,5285,8467,7663,5218,5291,7908,7628,9054,8479,5717,7528,5471,8903,0,8159,7920,7920,7482,8397,8381,8539,5347,7738,8154,8613,8052,5367,8387,7888,5237,5442,5325,5309,8052,5377,8227,0,0,8936,5377,5329,5296,8073,5337,8077,7482,7982,5305,5464,5578,5301,7511,0,7700,7675,5305,7834,5301,5329,7610,5564,7883,8033,8751,7920,7521,8154,5305,5237,7888,7692,7834,7877,7616,8261,7828,7986,7738,7675,5285,8287,7811,8154,5622,8077,7675,8936,5301,7877,7637,5301,5476,7632,7700,8365,7588,5237,0,5407,24,7605,64,8077,7700,836,2998,7994,5296,1311,5688,7487,2675,5407,5296,7528,7482,7823,8641,8641,5471,8756,2947,7592,1400,429,8827,7982,1620,8467,5337,2599,8653,8827,7994,5647,7994,8099,819,8658,2651,8093,7786,852,1783,8039,5647,5337,8039,7482,7643,7734,7628,9080,8467,8099,7768,7628,8745,8039,8732,7994,9085,7768,9092,9097,8467,8467,7864,9104,5647,9113,8641,9122,5296,5476,8467,9130,8375,5647,8387,9137,8805,8119,9146,9152,9157,8387,5437,7994,5717,9164,7478,7952,7920,5476,8539,9172,9181,5655,5314,9187,9193,9201,9206,8582,8227,7706,5034,9214,5325,9220,7972,7972,8282,9224,9230,8196,9235,7667,9240,8068,7565,5501,8543,8772,8795,7920,7571,8946,5358,9247,9253,9259,9263,7908,8539,5485,5407,8772,5301,7904,7478,5501,8863,8312,7533,5629,8653,5726,8772,7972,5480,8811,5255,7828,7795,8447,7658,7920,7592,7924,9008,5708,7511,5688,7715,7990,8131,5529,7904,7710,7935,8093,9272,5564,7982,842,7805,7986,7610,7610,5329,9278,9281,8174,9285,5426,7582,9288,2462,5329,9298,9301,5666,5671,7482,5730,2465,7999,5564,8687,5642,7888,9305,7952,5496,7924,7482,8048,8131,8467,5459,5661,8800,7592,9308,7864,9312,7828,9316,7478,7473,9320,76,2406,8509,2468,5386,8345,5426,9326,7811,7898,8456,9329,5476,7482,3679,7500,7805,8433,8845,7663,9332,9335,5699,5218,142,9338,429,8349,7843,4262,7558,7834,5309,2422,5666,8577,8201,7558,9341,303,5391,8329,8039,2274,7491,8184,8539,7628,8201,8387,5676,5342,7920,7558,5509,8170,8099,5241,8571,5248,7582,5491,8772,8345,7774,5320,5413,8795,7972,7888,5476,7972,5476,8456,5325,9032,7706,5514,8170,8447,7692,7990,5629,8653,8447,5407,8217,5309,8461,7628,7864,5352,7786,5647,8873,0,8387,7542,0,8345,0,7888,5386,5647,7558,7482,7643,5358,5401,8039,5459,5622,5730,8222,8222,7628,8539,7790,5381,8821,7864,8845,8795,8548,7628,5464,7864,5642,7834,8222,5325,5712,8227,8762,5480,7482,8527,7548,5381,5655,8527,8461,8732,5407,0,0,7521,5464,8873,8676,0,8190,5325,5325,7908,7616,7738,7558,5218,8509,7588,7588,5391,5651,7738,5534,7558,5647,5629,5699,7548,5301,5352,7898,8159,8205,8217,5606,7478,7558,8329,5717,5655,7663,7817,7834,8795,5606,8467,7752,5661,7511,0,7843,8227,5352,5413,8033,8140,5329,7742,7542,5712,5305,5622,8617,5320,7957,7710,7800,8473,5485,7706,5320,8539,8015,8795,5218,7828,8033,7972,5237,8039,8322,5309,8539,8987,7715,8402,7834,7738,8833,7706,8322,7779,0,8863,8255,8833,7500,5261,5261,7904,7734,7908,7582,8010,5699,5305,5471,5471,5471,8539,8119,7888,7888,8772,8919,7478,5501,5464,8772,8245,7491,0,0,7628,8322,5325,8255,8800,5476,7920,5358,7828,5419,7491,5218,8073,8772,5347,8170,8359,8903,8261,7542,8329,5347,5237,7849,7663,8387,8479,8052,8821,0,5666,8582,5329,7834,8772,7994,8359,7610,7610,7872,5301,7516,8261,0,8329,7834,5666,7904,0,5391,8504,0,9345,9345,8119,5476,8349,7990,7605,9345,7982,7982,8653,5476,7982,7500,8653,8087,8211,9021,8087,5476,7972,7786,5573,5419,8527,7930,0,8411,5342,8658,8093,5564,7571,5367,7504,0,5386,5367,8005,5237,8082,8467,7790,7588,7888,7888,7491,8745,0,8131,8131,7706,8249,8249,7482,8154,8033,8245,8115,5606,7658,5564,8625,5642,7924,8560,5524,5391,8543,7658,7853,7834,5358,5476,7473,7977,7888,0,8582,8267,7977,8994,8869,5401,7811,8539,7834,8174,7768,7706,8716,8527,5661,5501,7828,0,8005,7967,7811,7706,8509,7687,8543,5476,5419,8272,8205,8543,5237,7521,7893,7757,8329,8946,7828,5708,5617,5534,7533,5708,7478,5708,5419,5401,7888,8267,5419,5391,5501,8154,5358,5401,7888,8293,8293,5693,8716,7542,5407,7738,8484,7823,5496,8052,5274,8010,5325,8851,8613,7893,7599,8062,9345,5564,7538,7883,8504,5564,5485,7834,5661,7752,7548,8994,8762,5325,8946,7548,5642,5309,8267,7622,8456,0,8354,9345,8816,7834,8136,7528,5501,5358,7628,5480,8527,8467,7565,7478,7628,7952,8762,7904,8447,7576,5314,7999,8245,8716,8467,7853,8190,7972,7864,5712,5305,7706,5358,7914,7867,7864,7849,7500,8048,5241,8438,8282,7715,5564,8093,5209,5209,5391,8297,5261,7924,7800,5381,7786,7487,5426,7994,5209,8170,5261,8751,8504,5642,8316,5419,5320,5325,5671,8987,8144,5676,5285,8131,7715,5407,5407,5476,8184,7972,5325,5325,5274,7946,8687,7667,7473,7834,8391,8577,8456,7834,5337,7834,5496,7914,7482,7710,7952,7768,5209,8022,7994,8062,8631,7957,8387,7648,5285,8375,5255,5274,8670,5301,7558,7675,8211,0,5612,7478,7994,7990,8277,8287,5693,8929,5426,7823,7628,7495,8456,8028,5676,7914,7972,7653,8539,9345,7864,8751,8422,7653,7746,7478,7834,7478,5237,7834,7628,7487,7628,8631,0,5274,8170,5726,8839,7706,5274,8863,7582,8227,5329,7663,7864,7864,7957,7746,8827,8057,8170,8312,7774,5693,5509,8473,7675,5320,7706,5296,5564,7864,8494,8772,7893,0,5381,5606,8447,7768,5485,7643,5485,7542,7828,5426,7982,8631,8277,5358,7482,5442,8287,5564,5485,8099,7834,5347,8653,7482,8387,0,5301,7828,5274,0,5396,7616,7877,7914,8805,8903,5642,7478,7823,8893,7977,7853,7500,7628,7994,5377,8110,5712,7883,8821,8196,0,7478,7542,8104,7790,5564,5381,8676,5237,7548,5712,5464,7746,8345,7616,8190,0,7994,5712,8467,5655,5688,5377,5661,5480,5401,7790,8297,7487,7858,7994,8631,8484,7982,5708,0,0,7500,7853,7952,7967,5642,7920,5237,8467,7616,8164,8613,7710,7511,5712,7533,5708,7538,8131,8005,8196,7977,5514,5655,8467,5496,5218,8391,7616,5325,7588,5453,5274,8272,8334,7843,5386,8222,7663,7920,8751,5476,8647,7628,5666,8190,7800,5476,8015,8756,5218,8447,5301,8539,7952,7834,5309,5622,8227,8604,7653,5362,8149,5305,5578,7982,7982,5708,5362,8322,0,8959,7786,7478,5285,5480,8427,7790,5274,5274,5305,8239,5401,5396,7952,8919,7972,7500,0,5396,5237,5296,7648,5237,7834,7877,5285,5712,8010,8010,5274,5476,7994,5285,5564,5391,5358,5261,5342,8322,5401,7811,0,5453,7582,5476,8179,7663,8929,8766,7538,7681,5442,5309,7839,7628,8751,0,7500,8716,7834,7658,7616,8179,5453,0,7616,7746,0,7538,7914,5426,8447,8762,7511,8467,5347,7834,8617,8093,5285,8929,5347,5274,7616,8617,8952,7768,5651,5301,8375,7834,7605,5347,5209,5381,5291,5291,8571,8267,5209,5209,8879,7962,5564,7930,5209,7675,7528,8249,7487,7658,8222,5325,7786,8293,5529,0,5622,8370,8375,7962,8359,7763,8827,7542,5320,7877,8387,5209,8447,5564,5209,5301,5529,8073,8613,8277,8077,8077,7616,8375,7746,7605,8164,7738,7952,8533,7723,7628,8087,8345,5647,8022,5509,8110,5642,8062,5476,5485,9032,5651,7605,8903,8062,8548,8721,7576,5693,5509,5476,8222,5391,5391,5248,7914,7990,8190,5391,5337,7811,5476,7681,5391,5274,7817,7558,8658,7817,7675,5437,7687,8222,8370,7834,7675,7977,7914,8033,8164,5464,5651,5568,8739,8888,7588,7774,8365,5325,8467,5325,7482,5651,5568,8745,5699,5655,5407,5564,8381,5224,7811,5712,8196,7653,7628,5391,8381,8196,8381,7972,7628,8222,8739,8381,8196,5305,5496,8170,7795,8196,7982,7478,8239,5391,7605,8533,7914,7994,5661,8739,5296,8239,7667,7811,5476,5476,8222,8196,5248,8222,8527,8879,8217,7487,8329,8479,7672,8217,8087,8312,8527,5622,5564,5617,7990,5419,5419,5622,7994,9021,8329,5320,8217,8479,7675,5325,8670,7696,7696,7696,5205,5407,5320,8033,8987,8987,0,5205,8227,8239,5274,5274,0,7599,7599,5337,8239,8239,5642,8211,7599,7599,7599,5205,5642,7888,8005,7548,8484,8811,7786,8811,8422,8811,8811,5391,7482,7710,8898,8447,5309,7994,5453,8170,7920,8554,5237,5358,8170,5381,5606,5726,5377,5381,7478,7757,5391,5391,5255,5325,5661,8033,5237,8447,8427,5237,0,7800,7864,7957,5255,5377,7893,5320,5661,5647,5329,5329,5237,8554,5329,8170,5661,8170,5291,8239,5291,5320,5237,7935,8068,8447,5261,0,5274,5301,5529,0,0,7610,5476,7706,8504,7478,7653,7914,7628,5573,5342,7706,0,8354,8282,5642,8608,8387,7478,5381,7864,5325,7542,5501,8467,7706,8433,7542,7706,5496,5381,5726,8461,5274,5708,5347,8479,7904,7571,7571,7924,8170,8461,7990,8131,5529,7930,5301,7681,8170,7834,5564,5519,7977,8093,5480,8456,5337,5491,8582,8354,5726,7982,8312,8438,8345,7681,8093,7710,0,7582,0,8005,5519,7790,8316,5529,5476,0,7849,7990,7632,7864,5564,8329,5459,5329,8345,7696,5671,7768,8316,5476,7715,5391,8345,7994,7982,7972,5651,5301,5325,8316,7994,5337,0,5367,5301,5642,5285,5241,8375,5285,7864,7864,7628,7491,8201,8255,8658,8527,8527,5386,5301,5337,5274,8062,8479,7914,7558,5647,8845,7667,8052,5367,7482,7628,7843,8795,8149,8277,5274,5699,8571,5248,5248,7738,0,5396,8170,8277,7706,8438,8653,8653,8443,7786,8402,8387,8387,8461,8099,8099,8873,5291,7888,5241,8039,7482,7582,7653,5241,7994,5642,7920,7834,8022,5381,5413,8443,5291,7582,8427,7786,7482,0,8115,5464,7628,7994,7924,8527,5688,5471,5377,5476,7972,5391,5437,5642,7864,7605,8795,5329,7834,5712,8811,5325,5325,7811,8805,8119,5712,7542,7478,7628,8548,5347,5291,7834,7834,7994,8548,5496,5342,8164,0,8196,7628,0,7957,5647,8217,5237,5534,8777,8903,8062,5381,7663,7511,7478,8022,8255,5209,7994,5413,7828,8451,7542,5218,8077,8451,5325,0,0,8022,8548,5419,8239,0,7877,5459,8433,5693,5647,7957,8015,8427,8427,5708,8340,5726,8322,8397,8397,8479,8479,5237,7628,8073,7610,7521,8554,8073,8196,5285,7834,8149,8427,7516,8239,8239,5309,8919,7888,9021,5285,7952,5642,8255,5642,5291,8427,5476,5529,5386,7491,7491,5476,7864,5274,5325,5661,7990,5237,8539,5708,8397,0,8447,8164,8479,8093,8925,7628,8354,7482,0,5381,5301,5342,7610,7610,7828,7616,5237,8170,7877,7616,8005,7834,7994,5386,7768,8783,8498,8375,7828,7828,0,7834,7834,7834,5386,7834,8732,7952,8732,7576,5464,5485,8329,8783,8783,8577,7817,7571,8255,8170,7817,0,7478,0,0,7637,5688,5688,5325,5476,7487,7904,7972,7999,7521,5476,5309,8329,5676,0,8329,5309,5564,8925,5358,5391,8201,5693,8952,8010,8077,5391,5501,8994,7972,5237,7982,7883,7994,8015,5629,5491,7538,8456,8994,8964,5564,5401,7972,7576,8073,7972,8539,5305,5476,5476,5255,5280,5676,8613,8952,5491,5241,8839,8255,5655,5391,8658,5237,7898,5491,7952,5442,5255,8015,8302,5509,5491,7994,8227,8302,7706,8115,8039,7790,8762,5730,5241,7500,5651,7994,8494,5564,5476,7898,5255,0,7817,7533,5464,8479,5651,7977,8909,5296,7548,5261,7800,7715,8473,8039,8115,7715,7898,8015,7592,7877,5564,7972,7982,8010,8519,5358,0,0,5651,8255,7877,5501,8249,7898,5442,7763,5573,5381,5274,5347,8811,5255,8184,7542,5224,5224,5426,8811,5642,7811,5337,5337,5325,7834,7920,8805,8115,8687,9345,5305,8745,5358,8196,5471,8519,7952,8387,8126,5381,7500,5329,7478,0,7692,7616,8539,8174,7828,8387,7622,8845,7839,5564,8316,8334,7893,7521,5476,7972,7681,7972,5519,8762,7571,7675,5647,7952,7692,8334,8805,5337,8201,5693,7558,8277,7930,5325,8345,5320,8756,5218,5480,8022,5320,8745,7628,7628,7622,5448,8604,7521,7893,8484,7849,5296,7511,8302,5401,7843,5448,5407,8057,8745,8484,8484,8196,5708,8144,7957,7482,8239,5491,5329,5476,5573,7877,5647,7487,7528,7622,8227,7478,7893,8687,0,7908,5358,8322,5661,7516,7516,8239,5329,0,7616,5274,8805,5218,5301,8805,7482,8329,7834,8805,8539,8312,5407,5296,8716,5296,7817,8184,8509,8411,5476,7706,8789,5501,8170,7834,5476,8205,5358,5476,5476,7696,8589,5391,7696,5391,5391,7817,7757,7482,7982,5564,8062,7478,8422,7710,5419,7904,7914,8539,8015,5291,8272,5564,8539,5651,5274,7982,8783,8293,5485,5655,5476,5651,7768,5509,8670,8893,8190,5485,5471,8015,5419,8322,5352,7883,8322,5358,5358,5617,7883,5407,8131,0,8048,5358,5568,0,5432,7675,7883,8144,8322,8062,5651,8144,5509,5291,5325,5325,7952,8903,5329,5329,8641,5296,7972,5296,5296,5476,5476,5476,5476,8154,8154,5261,5426,7752,8316,7834,7834,7628,7710,7774,8473,7757,8888,5352,5218,8647,8015,7616,7710,7710,5314,5480,5325,8334,7548,5476,7599,7935,5358,7710,8093,5564,7599,5241,8140,7599,5485,5491,5237,8329,8727,7924,8359,5329,5730,8631,7558,7972,7500,5485,5647,8827,0,8277,8827,8407,5213,5237,0,7706,8227,7972,8827,7576,8277,7628,5661,5381,5407,7710,7977,8411,5464,5564,8329,7994,8334,5642,7994,5237,5241,8647,7710,8407,5241,5241,5432,8407,5248,7935,5285,7994,5285,5296,5712,7610,7521,8222,5726,7528,5320,5358,5476,5333,8504,8479,5693,5671,5325,8504,8504,7877,8772,8594,8594,8811,7706,5391,8879,7972,7599,8211,5237,8010,7616,5606,8438,8438,7548,5248,7478,5325,7478,5237,7491,5337,5509,8827,7548,7908,7628,8411,7972,7706,8402,8033,7658,8068,7542,8184,7643,7504,8816,7599,5274,7528,8756,7982,8509,5501,8359,7658,8022,7478,7742,8227,8519,8170,5476,8222,8783,8438,8456,7571,8982,7924,5568,8282,8293,5386,5241,8994,8201,8964,7786,7982,8170,8245,5401,7487,5442,8539,5491,5476,5647,5337,5564,5325,5352,5426,8467,8447,8625,5564,5237,5358,5337,7867,8190,8827,5224,5464,7500,5573,7920,5237,5237,8772,0,8548,8217,0,0,5476,7710,8504,8329,5391,5237,8149,7946,8582,7715,7972,5661,5401,7681,5464,5459,8052,8571,7952,7946,5237,7675,5320,0,7473,5688,7924,5296,5296,7710,7977,5476,7504,8625,7516,5296,5209,7500,7710,7521,8293,8727,8869,7990,8316,5606,7817,7768,5237,5280,8354,5352,8800,8407,5296,7864,0,8316,7834,8509,8604,5386,8267,8211,5218,5218,8467,7920,8653,8255,8354,7628,7628,5381,5237,5578,7516,5377,7653,8052,7867,5377,8201,5476,7542,5296,5237,7930,7800,5476,7946,8022,7768,7558,5296,7478,7478,5337,5301,7779,7811,8827,5476,8354,5301,8566,5386,7864,5699,8190,7542,7478,8438,8827,7893,7930,7888,8402,8227,8514,5301,5325,8721,8721,5426,8479,7706,8571,5606,8099,5237,8795,8653,8447,8745,5442,5471,8039,7877,7779,5509,8467,5309,5325,7516,7628,8154,7849,7768,8772,5377,5377,8077,7599,8287,8201,7643,5237,5629,7528,7994,5296,7864,7872,8548,5407,5218,7828,8170,8179,7558,8407,7930,7867,8179,7883,8721,7888,5386,8267,7521,8863,5459,5464,8104,7723,9071,5426,8604,8411,7643,8548,5377,8255,5442,7930,8190,7994,5717,7542,7994,7628,7628,5358,8190,7811,8438,5358,5476,7746,5218,8509,8903,7482,7864,8033,8711,7990,7990,7628,7663,7867,7834,7495,8811,8267,8548,8159,8873,5501,0,8005,8005,5699,7817,8267,7588,7588,7706,5642,8022,5464,5325,7786,8170,8519,8447,5661,8509,5241,5218,8340,7962,7914,8073,8509,8467,5661,7643,5218,7924,5301,5514,8272,7482,7839,7500,7542,7658,5464,8267,7914,8391,0,5617,8827,8647,5305,7715,8039,7710,8179,8179,8179,8402,8438,8594,8015,8316,5708,8617,7800,8721,5699,5372,8484,5296,8509,5401,7742,8022,5309,7482,5329,8987,7994,8641,7800,7542,5617,5218,8745,8756,8539,7972,7957,5676,7706,7706,7628,5255,8239,8827,5337,5248,7972,8316,7558,7706,5726,5352,7930,5432,8104,5285,8833,7710,5401,5255,8005,5296,8255,7558,7482,7482,5237,5237,7883,8772,7888,8217,8217,7588,5291,8604,7675,8010,5501,8149,8149,5248,7653,7706,5274,8438,5485,8239,7478,8179,5291,8140,5274,8772,8119,7920,5476,5573,5274,5386,7883,8772,5485,8022,7653,5285,8082,7811,7582,8196,8196,5726,8340,8800,5661,7616,5708,7790,5476,7516,7719,7667,8359,8514,5347,7663,7516,7834,7795,5367,7710,7628,7482,5491,8249,7663,8249,8217,7706,5305,5347,8514,5442,7972,7495,7495,7834,7924,8073,7972,8582,8582,5337,5564,7511,7763,7558,0,7877,8249,5347,8359,8190,8261,7877,5325,8170,7811,5617,5476,5401,7823,5651,5629,5291,8461,8461,8461,5717,8354,7478,5407,7500,5320,8164,5237,7558,8494,5381,5717,5407,8322,8154,8322,8427,7582,7706,7977,5459,9353,7977,5325,5391,8795,5325,8005,5671,8919,8427,8519,5717,5407,5381,7628,5407,7834,5329,5237,8427,5237,5237,8287,5491,5726,7982,7982,7982,7864,5391,5301,7790,7982,5301,7565,5612,8411,7828,7592,8987,5476,5476,5261,7982,7893,7834,8087,9358,7774,7687,5688,5333,8201,7904,8245,8705,5564,0,5437,5480,8015,7757,8756,7628,7692,7653,7914,5496,5476,7786,5568,7478,5480,7542,7972,8504,8144,8514,5514,7924,8816,8282,7935,7648,5237,7542,8282,7977,7628,5564,5708,5237,7920,7972,5209,7864,5471,7999,7558,5325,7834,8582,8411,8783,8170,7752,7982,8354,5291,7904,8196,5655,5241,8267,5509,8293,5476,5320,9361,8057,7853,8307,8756,5578,8057,5564,8174,7904,5708,8115,8641,5568,8099,8322,0,7982,8222,5224,7972,5352,8772,7734,7920,7658,5642,5655,7687,7924,8222,7849,8447,5476,8539,7710,7920,8925,5305,5712,7914,8387,5442,5726,8438,5573,7977,5564,8427,7605,7883,7637,7715,8184,7986,5564,8174,8282,5213,8093,8136,8625,7542,8170,5480,7904,5480,5480,5708,8170,5314,7533,7999,8126,8126,8539,8447,8170,8144,7610,5407,0,5218,5347,8381,8073,8115,7542,8727,7795,5391,5381,5241,8566,7817,7817,7768,8969,8879,8365,8898,7710,5661,8604,5296,8751,5635,5391,8136,8073,7710,7795,7972,8144,7888,7888,7972,7867,8345,8789,5655,5717,5285,7692,7582,8613,7681,8577,7687,5329,5459,8631,5291,0,8170,7972,8375,8387,9361,7738,5661,5386,8903,5285,5241,5325,5296,8329,0,7663,5480,7495,8451,8073,8845,5377,5655,7558,5612,5612,8946,5712,7610,8170,5352,5407,5476,8805,5261,7495,5509,5661,5496,8527,5285,7478,7667,8484,8316,8653,8154,5647,5314,8057,5333,7904,7653,8670,8077,5676,8115,8022,7491,5291,8201,7811,8201,9021,5476,8533,7672,5448,5480,8149,7962,7898,5407,7811,8479,0,5509,8196,5352,5617,8577,8438,8514,5564,8126,8170,8479,8494,7663,5676,8354,8548,8033,7805,5381,5647,8543,8136,8184,7495,8443,8494,5237,5274,7558,7828,5514,8099,8745,8461,7888,8087,7757,8057,8539,8015,8234,5325,8272,5280,7706,5213,5320,7542,8093,5476,8387,5291,5241,7779,7795,5476,8154,5261,5241,5329,7774,8772,7935,5224,5381,5377,8571,8422,7834,8451,7967,8170,5606,7542,8800,8126,8087,5381,5329,8039,0,0,7972,7883,0,5274,5514,8721,5352,8467,7495,5407,5655,8484,5407,7628,7834,8005,7823,8062,8110,7920,7994,8159,7967,8190,7622,5377,8077,5496,5391,8548,5655,5476,8598,7946,8411,7924,7811,7834,8857,8345,7500,8387,8494,7990,8663,5325,5377,7986,7986,7558,8509,8484,8484,5647,5381,5296,8845,5248,8851,5617,5661,8467,7542,7491,5496,7687,7930,5642,0,0,5347,7752,8196,7877,5717,7957,8548,8845,5285,8131,8196,5218,5699,5442,7710,8427,7962,7962,8073,5573,8164,5413,8451,5514,7990,5642,8479,7972,8293,5661,5407,5448,5688,5524,5274,8851,8940,5309,7763,7864,7805,7495,5261,7738,5381,5629,7533,8282,8245,5301,8272,7982,7972,8631,5367,7542,7752,8322,5606,5655,7864,5237,7495,7768,5496,7687,5717,7478,8869,5617,8756,5617,5320,8126,5642,8888,5448,8772,7730,7877,5612,8239,5248,8519,8039,5224,8527,8952,8539,7706,0,8533,5401,5629,7710,8721,5329,7632,7957,8548,5671,7982,7687,8519,5629,5396,8402,8716,5305,5419,7952,7616,7972,5708,7972,8077,7800,5261,5568,5320,5612,8641,5285,5305,5305,5578,5274,8946,8647,5419,7482,8131,7542,5329,5325,7616,5291,8959,8959,8149,5471,5274,8821,8005,8365,5476,8329,7558,7558,7730,5386,7946,7487,7542,5464,8245,9021,8184,7516,5241,5296,9345,5285,5325,8340,7883,7883,7935,8073,7935,5291,7675,8239,8543,5564,8255,8073,8451,5314,5622,5480,7734,8196,5471,8184,0,5661,5329,5285,8498,7738,7653,8255,9361,5407,8811,5218,5666,8144,5329,5261,5314,8115,7734,8287,5325,8903,5612,8340,8745,5485,7811,5381,8196,7908,7883,5237,5717,7920,8073,5708,8170,7491,0,0,5448,5651,5325,7658,5314,8479,5426,5347,8936,8170,7491,8613,7491,7491,8447,7982,5325,8888,8381,5717,7972,5381,8144,5305,5407,5661,8577,8772,8126,5448,5329,5329,7648,7972,7533,7924,5666,7730,5320,8365,8184,5274,7667,8154,5301,7986,7653,7616,5329,7610,7542,7542,8365,7542,5347,8589,7616,5712,8261,7877,0,8498,5237,5617,7632,7667,5651,5301,8365,5329,8772,8857,5301,7828,8631,8903,5285,5291,5347,5301,7700,7972,7952,7952,7706,7528,7734,5342,8087,8898,5337,5671,5476,7817,8811,8282,7565,5241,8073,7828,5568,8438,7478,7786,7849,8604,7675,5651,5573,5717,8539,7500,5285,8438,8033,8484,5655,7888,7628,7710,5708,8126,7500,5285,7888,5401,8077,5358,8227,7828,7888,5476,8811,8589,7990,7834,5471,7706,8307,7675,7888,5285,5485,7924,5367,8438,5320,7663,8255,7663,5241,8539,5655,5564,5320,8239,5726,7504,5237,5285,0,5285,5314,5476,5401,5358,5301,7675,7675,7752,5529,7706,7706,8533,8888,5291,8533,0,7893,8282,8282,5347,7628,5347,7994,8307,8119,8239,7977,7982,5661,7977,7628,5655,5285,8494,7853,5573,5309,7710,7482,5564,8322,8427,5726,5314,8427,8676,5314,7487,7528,5314,5314,8196,7516,5314,5573,8676,7605,7565,5661,8560,7628,5391,8456,5337,0,8334,7706,8427,7790,5329,8456,7920,7511,5564,7565,8190,5485,5485,8447,8443,7999,5476,7511,8670,5407,0,5476,7706,7972,8010,8647,8010,8427,8625,5342,7628,7478,5325,5325,5325,8154,8915,7883,8484,7972,5372,5661,7681,8329,7500,7500,7500,8811,7653,7658,8539,7500,7877,5391,8582,7924,5391,7599,8438,8438,8068,8104,7478,8267,7930,8608,8115,8282,5381,8653,8119,7982,7565,8052,7952,8227,8190,8391,7643,5501,8196,7982,5476,5237,7528,7834,8115,7565,8772,7914,8170,7924,8365,5224,5426,7616,8190,8456,5655,8093,8104,8052,5712,7571,5305,8411,7500,7643,7999,8548,5391,5391,7628,7893,5459,7790,8447,8227,7752,7582,7663,5224,8608,7576,7610,5274,8582,7681,8783,5391,5464,5464,8190,8005,7500,5285,5309,7972,5309,8987,8316,8062,7834,5671,7521,8635,7675,7491,8267,5296,7653,7653,7667,5509,5274,8653,7576,5237,7558,8119,8201,5578,5578,7478,7653,5296,8052,8033,5509,5606,5314,8397,7576,7734,5485,5237,5261,5476,5237,8745,7706,5485,7952,5655,5476,7972,8345,5509,7516,7779,8227,7599,7675,8365,0,8190,8190,5476,5367,8267,5296,5564,8438,8005,7849,7616,8762,7994,5320,8745,7790,7823,5407,8699,8345,7924,8104,5642,5612,5309,8391,5285,8509,5524,5534,8227,7706,8052,7834,5301,7663,5325,7952,8514,8365,5661,8052,7715,5305,8827,8447,8015,5296,8222,7653,8190,8732,8239,5274,7883,5485,8959,7516,8068,5237,5476,5325,5464,7658,7491,7491,5301,8772,8316,7675,7893,8196,7999,8052,5476,7521,5426,9367,5285,8222,5485,7834,7663,7491,8613,5320,8249,8329,5642,7734,5301,8033,8582,5459,0,7834,8154,5329,5237,8164,7610,7700,7632,8375,8635,5629,7834,5573,8354,8354,5726,5476,8845,7491,7982,7849,5320,8340,8340,0,7800,8554,8554,5377,8397,8179,7500,5296,7482,7867,5501,7920,8354,5337,8821,5352,8282,5564,8093,7893,5726,0,8293,7681,8851,5655,7843,7843,5676,8391,5464,7599,8068,7999,5325,7706,7616,8548,7599,8316,7843,0,8249,7610,7715,8919,7994,5305,8543,7687,7605,7605,0,7605,8504,7904,8312,7888,7888,8479,5301,5688,5325,8484,5358,5519,8805,7839,8302,0,8925,7768,7542,7628,8811,7478,5496,5261,8329,8010,7478,5209,5301,5426,7774,8879,8504,8670,5480,8539,8015,8211,7710,5476,7786,7817,8762,5476,7706,5320,8461,7681,0,0,0,8277,0,8211,7924,7538,7622,7487,8467,8560,7893,7930,5381,7582,5524,5642,5274,5573,8658,8994,5320,5396,7528,5442,7605,5442,8022,8282,8022,5325,8354,5309,8131,5325,7790,7920,5358,7599,8608,7565,5501,5476,5564,7823,5237,7982,8387,8307,8329,5655,5642,7516,7872,7864,5712,7999,8039,7972,8827,8267,0,8307,5320,5372,0,5301,0,5407,7558,0,8322,8527,7823,7675,5274,7692,5255,5476,5237,7972,8625,7675,7977,7977,8131,5564,5305,5476,7491,7491,5391,8077,5358,7888,5476,8093,5224,8447,7986,7849,7533,8010,7548,5491,7839,7924,5564,8073,8711,8811,7706,8297,8110,7790,7999,8022,5578,5367,5337,7914,8170,5480,7571,7823,8381,5218,8539,7972,7687,5496,7883,7786,0,8484,5708,5564,5407,8073,7888,7990,8716,7605,5358,7511,7637,7687,7487,7605,8456,5391,8625,5496,5642,5291,8484,5391,7972,7511,5712,0,8164,7687,0,8473,8427,5381,7610,7582,5329,0,5301,5347,5218,5320,8329,8969,7706,5655,7706,5301,5476,7521,5655,5301,7588,7795,8795,5224,8504,7675,8073,8504,5224,5209,5442,5476,5381,7500,7605,5407,8397,7696,7696,5337,8048,7817,8898,8149,8427,5301,0,7924,7849,8682,7972,7710,5717,5437,8375,5564,5381,8560,7952,8272,8687,7994,8582,8613,8345,8345,5676,5476,5314,7692,7972,7723,8316,8940,8940,7811,5381,8484,5529,7888,8863,7628,8365,0,7616,0,0,8427,8387,5386,5655,8903,7706,8255,5396,7908,5419,9345,8329,5612,8302,5655,7930,8805,7795,5485,8073,8543,5564,7872,8316,5291,5237,7843,7511,8527,7706,7752,7667,8811,8140,7779,5688,8805,5337,5712,7898,7610,5352,9345,8539,8190,5647,5372,5708,7786,5358,8539,7653,5209,7972,8504,7930,8845,5407,7811,5509,7495,7977,7681,5325,7675,5329,7811,8670,8433,5501,8022,8249,7478,5301,5301,8307,8433,7616,5612,8170,5377,5381,8653,5314,5320,7877,8010,7994,5485,5301,5224,0,0,8272,7972,8277,5401,5407,5314,8093,5301,7706,5224,9032,8827,7542,7746,7706,7706,7774,8234,7877,8594,7893,5213,7898,8057,7643,8548,7558,5372,5325,5301,5352,7628,7628,7786,7643,7582,5629,8461,7599,8539,5237,8010,7558,8427,7706,8467,7834,7628,7768,5476,9367,8504,7805,8873,7706,5314,5564,7990,8277,7967,7967,5314,8721,5501,5501,8033,7723,8077,5509,8039,5329,8052,5381,5476,5442,8170,8170,7558,7571,7779,8322,7972,8863,8387,8170,7930,5285,5314,5209,8888,5352,5693,5573,0,0,0,5407,7952,5237,8762,7542,0,0,0,7632,7972,0,0,0,0,5352,8427,0,5480,9367,7888,7491,8998,7994,7994,8676,7977,8548,5407,5476,7914,5237,7834,7834,8732,8427,8104,7628,7849,7687,8494,7858,5358,5642,8154,7500,8893,7924,7986,7834,8543,7643,8005,5381,8005,7487,8527,5661,5480,7972,5407,7715,7790,5573,7982,7982,5401,8873,8805,7823,7628,5337,5712,8022,8190,8190,7616,5655,7858,7491,5325,5381,7491,5573,8598,7628,8694,7994,5712,8539,5325,5381,8509,5688,7616,8110,7946,7957,5333,0,0,8751,0,5671,8772,5347,8267,7628,8033,5329,8170,0,0,0,5661,7977,5476,8427,5564,8716,5651,7843,8164,7786,5407,8170,7738,5642,5655,7898,8302,8467,5209,8391,7588,7977,5642,5381,5464,8811,7893,7687,7542,7538,7962,8467,5661,8164,5391,7972,8402,8267,8805,7511,8164,5629,5699,8533,7487,8119,7738,7990,5237,7786,7706,8898,5524,7962,7533,8272,5717,8329,7828,8417,5325,8851,8131,7805,8322,5496,7542,8359,5712,7500,8461,5524,5314,5480,0,7924,7828,7977,5407,8397,0,0,8888,7605,0,5237,7706,5401,5296,5218,7542,8039,5671,5671,8641,5396,8140,9367,8402,5413,5337,7982,5419,7982,7811,8005,5476,8077,5524,8756,7952,7952,8340,7800,5309,8539,9345,5333,7843,8033,8827,5301,5519,8745,8617,8010,8316,8721,5325,5612,5666,5329,7779,5651,7957,8783,7588,7990,5320,5629,8473,5329,8484,5320,7687,5320,8015,8514,5305,5453,7924,7681,5352,7605,8302,5237,7768,7946,5407,8827,0,0,8322,0,7616,7696,7528,7972,8255,8397,8827,7734,5320,5396,8005,5655,9021,8354,8427,5237,8329,7828,8048,8811,5305,7478,8255,5386,7516,7511,8255,5274,8489,8010,8427,5480,8149,8267,8598,8422,7558,5629,8249,8170,7558,7588,5291,5291,8772,5296,5325,7495,7521,5480,8381,7872,7605,8519,8833,5501,7723,8297,7946,8433,7786,7511,8149,5524,5573,0,0,0,0,5325,8307,0,5329,0,0,8467,7738,5320,5419,8772,7516,8789,7482,5237,5342,5261,8397,8190,7558,8811,8170,8397,5476,8196,7734,7834,5666,8527,5296,8903,5325,5342,8073,5358,5693,5573,9361,7908,5325,5381,7977,8745,5578,5407,8322,8479,5476,8473,8022,5617,7516,0,5496,8249,7482,8272,7658,7746,7706,0,8888,8154,5325,7542,5651,8381,5476,5524,5407,7482,7696,8745,7834,7706,5651,7999,7952,8104,5325,8005,8312,8919,5573,0,7746,5337,7972,8687,7542,5666,5666,7730,5329,5301,8498,7672,7763,5296,7742,5329,5381,5524,8762,8467,5301,7893,5314,5448,5401,0,5573,0,7605,5329,7834,5301,7605,8033,5381,7864,7588,8359,8077,7628,7610,8277,0,5274,0,8170,7675,8093,7616,5407,7482,5337,7616,7521,8789,8073,7877,5347,8170,8617,5617,7994,8827,7768,5726,0,0,7628,7632,5301,8365,8375,8903,5291,0,5347,5347,7605,5666,5301,7904,7700,7628,8093,0,7542,7839,7811,7528,7628,7628,7542,7742,7500,8783,5573,7710,8509,5476,5291,5237,7500,5367,7628,7908,7843,8739,8851,5237,7663,5476,7706,7982,7710,8196,8739,8795,8739,8795,8732,8302,0,7482,7528,5661,7628,7542,7675,5476,5717,7478,8772,5309,7742,7982,5377,7811,7500,9054,5237,7675,7500,5237,0,5401,5573,5476,8795,7628,8287,7811,7972,5241,7839,8201,7478,7706,8201,8201,8082,8170,7542,7710,7719,7710,7719,7548,5342,5342,5301,8451,7628,5301,7952,5491,5491,7487,5407,5578,5305,5305,5642,5305,8140,7924,5606,8119,5491,8987,7500,8329,5320,8179,5291,8647,5280,5688,7588,8745,8267,5401,5476,8998,7972,7491,8179,7643,7883,5325,7491,0,8267,7752,5442,8888,5377,5309,8987,7482,7757,8140,5296,8498,5476,7883,8762,5419,5401,8888,8762,0,5407,5320,7742,8676,7605,7482,8745,5476,8302,5699,7990,5642,7667,7828,5296,5296,7786,5699,5325,5476,7742,5296,8676,5476,7675,7478,8721,7715,5712,5712,7663,7571,8762,7628,5358,5358,5358,7920,7920,5358,7877,7790,0,0,7734,0,7977,8427,8159,7715,5480,8329,7696,8164,5274,8170,7715,8052,5688,8307,0,8772,8456,8170,7888,8402,5358,8589,0,7790,5342,7628,7628,5480,7849,5407,7738,8589,5329,7715,5301,8316,8164,8227,7734,7632,8427,5514,8427,8249,7632,5726,5459,5391,8539,8438,5291,8211,7706,8959,8716,7706,8479,5274,8732,5501,8915,8028,0,5564,7914,7582,8745,7706,7482,5237,8370,7924,8345,8387,7990,8745,8387,7582,7883,5329,8745,7924,8267,8267,8839,8589,5241,5224,5218,8903,0,5333,8447,8068,8010,7952,8805,7972,5688,8888,8115,7746,8498,5296,7757,5476,5496,5314,8732,5509,8010,5491,5237,5480,8211,5688,7706,5261,5213,7653,7542,7478,7478,8329,7487,8613,7817,5419,7710,7478,0,0,5629,7972,7710,7542,5651,7628,7599,8745,5485,7872,7667,8062,7658,7768,5325,8411,8411,8811,7752,7548,7565,7504,5291,8560,0,7706,5501,7500,7500,8196,5476,8494,8267,8467,8560,7834,8033,8033,7883,5337,5471,7982,8154,7867,7715,8110,5712,7738,7746,7478,5248,5606,5358,8302,7972,8456,7533,8863,8925,8131,7511,5712,8115,8527,7742,5301,8099,8777,8527,7994,7982,5305,5712,5320,8170,8365,5480,5480,5480,7849,5301,5651,5476,8048,7893,5337,5606,7972,7999,7786,8427,7834,7487,8582,7592,8170,5661,5688,5476,7977,7548,7768,8073,5296,5651,8543,5314,5573,5333,5337,7786,8062,8811,8048,7790,7487,7982,5255,7839,8010,7710,7588,8381,7558,8093,8174,8073,7706,7487,8447,8170,8354,7605,8119,5666,8190,5529,5564,7533,8438,7658,7924,5325,5218,8721,5413,5529,7610,5329,0,8073,5661,8312,8115,5485,7482,7576,5325,5476,5296,5296,8397,7558,7482,7994,5730,5407,7924,8577,7487,5325,7696,5426,5564,7730,5612,7972,7482,7478,5237,8898,7605,5419,7632,0,8345,7582,5476,8711,5666,5476,5651,7768,5309,5305,7994,7675,5661,8062,7500,8851,8582,8272,7867,8170,7696,7576,8613,5255,8329,7504,7972,8687,7811,8391,8154,8631,5280,5213,7478,5726,7511,5476,7977,5485,7542,5419,5285,7558,5391,5314,8604,0,7908,5325,7511,7877,7643,7982,8789,8329,5309,8577,5688,7994,5578,7667,7653,7898,8211,5314,5337,5237,8073,8307,5509,8062,5476,7734,7616,5347,8052,7843,7653,5301,8845,5564,7478,5401,8104,8527,5476,7628,8316,8149,8370,5309,7977,5248,5534,5712,5342,7558,8456,5274,7914,5647,8653,8751,8077,7653,5485,7675,5617,8184,5647,5218,7675,5274,8805,7864,7588,8068,5676,8433,5377,5301,8839,7487,5666,8839,8272,8762,8149,8277,5401,7994,8494,8267,7811,8539,0,8489,5407,8015,7719,5564,8721,5426,8087,8969,8387,8287,8015,7491,8571,8494,7653,5480,7582,7849,8494,8170,8811,7779,7643,7738,5314,7828,7867,8407,5676,5209,5237,5642,7706,7786,8039,7653,7516,5629,7576,5352,8052,8227,5320,7706,7565,7710,5329,5386,8170,8272,8827,5666,7972,5352,7893,8022,5606,7920,7883,7628,8447,7628,5329,8811,5651,5485,8093,8099,7482,8402,5726,8359,7628,8184,7914,8647,8104,8631,8467,8277,7643,8745,7834,7542,5606,7622,7972,7482,5651,8543,8467,7752,9071,7893,8745,8467,7990,5325,5617,8467,0,5329,5209,8762,0,5407,8438,7920,0,7628,8467,7914,7914,8110,7811,7605,5407,8699,7628,5688,8805,5726,8762,8527,7687,8073,8494,8494,8539,7994,7752,8613,5661,8604,7994,5717,8461,7834,8062,8190,5651,7990,7790,7616,7834,5337,8062,7622,5426,5712,5437,7643,8716,8005,5391,7864,5213,5712,5401,7706,7706,5712,5606,8694,7994,8179,8641,7746,5296,5305,7500,5325,9054,5407,5407,7982,8527,7521,5476,7482,8427,5391,5381,7558,5464,8772,7628,5358,8438,5391,5647,8119,5218,5726,7548,0,8484,5285,0,7482,5629,7610,8154,8467,7823,8282,5337,7768,7805,7779,7877,7834,8104,8077,5209,5509,5391,7800,7616,0,8005,0,5407,7500,7672,7952,5671,0,0,8312,5309,7952,5629,8467,5612,8467,5401,5642,8249,9032,8119,7990,7658,8245,8631,7511,8131,5514,8391,7687,5391,5476,8099,7538,7734,7994,5301,7774,7478,5237,7752,8322,8115,5391,7588,5476,5655,5448,8267,5519,7482,8164,7643,7588,7588,8447,7738,5391,8613,7994,7533,5573,5573,8334,5534,8777,7763,7849,7828,8052,5464,5237,5209,8539,7972,7616,5564,5381,5396,5717,5209,8272,8909,5342,5358,5285,5296,8234,7482,5524,7610,7972,7834,5617,7800,7616,8104,5329,5347,8375,0,8888,8772,7628,5448,7982,5358,8473,7972,5419,5568,7914,8987,8539,5651,5708,5329,7982,7982,5337,7877,8010,5218,8234,7800,8745,7628,8647,5325,8387,5285,7706,5237,5325,5329,5329,7610,5309,8039,8827,7957,7972,5671,8322,5301,7834,8340,5285,8827,8658,8641,7742,8909,7491,5237,5651,5629,5305,8987,7653,5401,5347,8631,5301,7542,8527,8190,5476,7800,5209,8211,8756,5666,5352,5320,8015,8015,5248,7500,7811,8144,5407,7834,8519,8239,8402,5377,7924,5419,8827,7616,8062,8329,5320,8721,7616,7675,0,0,7500,5401,7972,7994,7920,5291,5285,8312,8903,5296,5381,7478,8994,5325,7533,7952,7675,7542,7542,5237,5329,5237,8190,5471,5464,8438,8489,7588,8239,5655,7548,5476,8539,5509,7883,8772,8783,8329,8005,0,8670,5647,7605,8589,9372,5358,5261,7877,7877,5320,8316,7734,7516,7994,7719,7719,7558,7592,7558,5396,5285,8149,7935,8811,8879,7946,7487,5320,5352,7653,8267,7511,8062,5564,8052,7849,5274,5407,5261,5237,0,0,5347,0,7628,7516,8438,5372,8903,5509,8115,5661,7628,5301,5391,5529,8093,7800,5209,5218,7908,8179,5401,7828,7491,5708,7811,5655,7877,7800,7883,7883,5476,5342,7920,8170,7738,8196,5325,5314,5401,8789,8039,5309,5501,7719,5325,5342,5296,5573,7738,8022,5693,0,7605,8277,8888,5401,5651,5391,5320,5651,8381,7616,7616,8249,5309,7706,7920,5325,8052,8613,8397,8494,8272,7482,7482,8217,7839,8154,5485,5347,7667,7663,7734,5314,7994,8888,7786,7542,5426,0,8144,7893,0,0,5407,7994,7632,8365,5329,5301,5337,7924,5329,7487,5381,7616,8762,7599,5296,5661,7533,7914,8473,7877,5305,7511,5407,7734,7719,7558,7786,8888,8154,5274,5329,8196,7653,7616,5218,5301,5485,7675,7616,5342,7994,5661,8359,8052,7605,7610,8277,8217,8010,7982,8277,0,0,7616,5651,5325,5401,5407,7616,5501,7516,5476,5407,8354,7877,5491,8261,5407,5347,8010,8617,8467,5314,8527,5617,5666,5347,8827,5726,5218,7542,8048,8721,5699,7834,8052,0,7730,8365,8375,7834,5301,5651,5261,8772,8903,5285,7877,0,8052,7904,7605,5347,5347,7700,5509,8375,8329,5274,7834,8811,7834,7628,8154,7487,7763,8687,8919,5296,7675,7675,0,8062,8093,5372,5622,8484,7687,5661,7710,5314,5437,5437,8179,7924,7706,7548,5501,8816,7592,7752,8312,5476,5329,7805,7920,8509,7533,7834,8509,8334,7715,8783,8239,7952,8756,8068,8687,7528,7972,8687,8687,7994,8322,0,8245,5367,5241,7605,7990,7999,5564,7904,7888,7888,8093,5314,8190,8170,8772,8174,7592,8174,7658,7658,5224,7924,8316,7653,7605,7687,7828,7994,5391,5401,7710,5464,0,7982,8851,5347,5485,5285,7500,5426,7768,8577,7811,7924,7675,5519,5274,7482,7478,7576,5642,5391,7994,8316,5529,5437,8509,8062,7632,5367,7605,8190,5241,8312,7834,5296,8687,5578,5296,8898,0,8010,8670,7491,5296,0,5629,5309,9071,0,5237,5564,7592,7935,7734,5485,5314,8805,5509,8370,5352,7843,5261,7982,8527,7710,8164,7982,0,5320,7849,8329,7692,5291,7675,5237,7834,7653,9345,7667,8539,5661,5325,7478,0,0,7653,7834,7811,8514,8170,5442,7930,5501,7516,8653,7643,8068,5237,8402,7478,8062,8170,5564,8494,5285,8827,8527,5255,7723,8893,5237,7723,7528,5314,7834,7834,5476,5291,7628,8461,8297,7920,7786,0,8119,8227,8670,0,0,0,0,8033,7849,8005,7548,8676,5367,5296,8527,5401,8190,5426,5464,5285,5407,5358,7616,7605,7994,8641,5606,5237,8699,8699,7877,7834,7592,7592,7628,5712,8302,7994,7491,7528,7723,8110,5712,7811,7500,7999,7994,8010,5688,5342,7924,8577,7605,0,5524,7795,8222,7982,8267,5367,0,0,8411,8131,8422,5629,8783,5261,8869,8073,5464,5661,5237,7478,5237,8267,7849,7849,5237,8267,7504,8716,5261,0,5642,8329,5651,5401,7616,7478,8164,0,0,8322,7511,5237,5358,8473,8548,5329,5407,8087,5476,7957,8527,5342,5476,8316,5291,8548,8267,7982,5476,7972,5708,7994,8239,7653,7742,9071,7592,7800,0,8987,8498,8833,5407,5237,5237,5237,5476,7675,7710,5464,7877,7491,7542,8297,7834,5274,8255,9021,8919,5471,7914,5285,5501,7487,8239,7628,7516,8316,8670,5501,8010,7834,8329,5347,5712,5629,5476,5485,8514,0,0,7616,7628,5476,5529,5347,7658,5372,8179,5342,5237,5699,8827,5401,5296,7605,5661,8093,7994,5476,5661,7999,5296,0,0,5347,8613,5274,8898,8329,5309,5442,5442,5642,7849,5347,8473,8164,8987,5305,8222,8893,8015,8589,5708,5367,8582,8851,7628,7746,5329,5442,5442,7834,5237,7920,5391,5391,5688,8261,8354,5347,7582,5291,8316,8721,5688,7779,5651,5629,7779,7834,5224,8093,8498,7628,9361,8387,8994,5241,5325,5381,8467,7663,7994,7877,7516,5291,5564,7491,7904,5459,7491,5642,8443,8617,7696,8322,8519,5717,8205,8716,8322,8484,8322,7858,8282,7533,8670,5407,5213,8762,7982,7853,5333,7548,5459,7898,8255,7982,5337,7558,8762,8387,7558,8816,7763,8751,8484,8711,5480,7715,8149,8504,9377,7565,7790,7715,5391,7478,7834,7977,7930,5391,5407,7786,5337,8375,8119,7715,8539,7542,7768,7768,5209,8594,5237,8211,8946,7542,7478,7786,5237,5301,5301,7516,5325,8461,7972,8543,7768,8489,8762,8170,7977,5407,8527,7858,7990,8170,8898,5573,7478,7930,7632,5381,7538,8682,7994,5301,8387,8316,8387,5419,7853,5301,8762,8577,7920,8647,8222,5237,5442,7994,8005,7521,7768,8010,7904,8010,5688,7491,7774,8073,8345,5381,9345,8687,7538,5337,8756,8504,5325,8387,7599,7811,9345,8267,8582,8816,8539,8745,7548,5442,7930,7920,8170,8447,8119,8174,5367,9008,7982,8093,7893,8777,8447,5391,5337,8131,8354,8170,7883,8312,7786,8316,8381,7811,8727,5285,7849,7999,5325,5476,5261,8227,8164,8087,8316,8149,8762,5367,5391,5396,5241,7628,7962,7628,5358,7478,5237,5676,5301,5367,7482,7940,7843,7930,7972,7834,8302,5301,8456,5274,7757,5471,7752,5642,7904,8756,8447,8473,7482,7478,5237,5407,8048,8653,7834,8039,7883,5651,8227,5476,7972,5241,5296,8249,7774,8140,5617,7994,8732,7632,7982,8548,8987,7994,8527,8494,8329,5426,7605,7790,7994,8467,5274,8641,8387,5386,5661,7977,5730,8411,5337,5337,5712,7687,5642,7962,8073,7977,5396,8467,5448,7663,8467,5491,7628,5391,7883,7605,8201,5655,8519,8670,7800,5485,5419,7715,8015,7877,5708,5377,8427,7849,5358,5291,5358,5358,5464,7734,7571,7628,8316,5642,8239,8851,7675,5218,8631,5237,5717,5676,8762,5296,7908,5274,5661,7738,5274,8329,8381,8048,8154,8417,7696,8577,5386,8687,8087,7952,7738,5274,5651,8261,7811,5386,7632,5274,7706,5291,8190,7622,5209,5329,5329,7628,7588,7599,7482,5391,5693,7605,7500,5325,8062,8539,7768,8811,8484,7628,5496,7710,8329,7538,7576,7478,8613,7752,5381,8387,5501,5329,5413,8354,5291,8604,5291,5509,7637,5712,7883,7920,7994,7811,5274,7558,8093,5491,5476,8345,5255,7828,5564,8190,5237,8073,7839,7823,7977,8670,8312,7628,7990,8297,7864,7914,5476,8443,7977,8438,5261,7511,5358,5655,5655,5291,8048,7610,5309,5285,5407,8589,8687,5274,7576,7817,7834,5519,7924,7500,7632,7487,7605,7528,5519,8329,5377,7696,5442,5642,5367,7946,7952,5248,7500,8282,8456,5564,7849,8170,7643,5213,7864,7558,5377,7893,7478,8387,7667,7930,7930,8805,5274,8287,7653,8190,5224,5688,7768,7843,5301,5519,5564,7511,7491,5237,8093,5301,5606,5401,5612,8514,8479,8772,5291,7805,8427,5352,5629,5730,8170,8033,5377,7752,5485,5381,7828,5573,5314,8456,7558,7982,7706,8548,8653,8227,7706,5413,5509,5329,8827,8721,7834,7757,7930,7516,8196,7920,8994,8716,7774,5407,8048,7491,7723,8302,8745,7643,7628,7790,5712,7834,8190,7548,5426,7977,5381,7994,5655,7994,8893,7811,5381,8732,5407,5622,7924,8494,7768,5367,7935,8467,8795,5386,7982,7616,7628,7930,7872,7719,7511,8427,8164,5612,5642,5407,7500,5309,7839,7616,7538,7972,8548,5629,5301,7843,7952,5655,8322,8451,8604,7999,5642,7663,7853,5647,8925,5329,5622,8827,5309,5476,8402,8827,7972,5301,5367,5612,5688,8190,5708,5401,7800,8015,7538,7898,7616,5629,5329,5381,8919,5261,8833,8329,8777,8903,7516,7930,7723,5218,5464,8772,7734,5485,8010,5309,7511,5325,5437,5485,7663,5358,8179,5726,5372,8903,8402,8234,5372,5296,7811,7719,5342,8732,7628,7628,5309,5329,8888,5485,7752,8397,7653,5237,5325,8190,8821,8154,5671,5666,8647,5301,5401,5329,7800,7542,7723,5578,7616,7582,5622,7610,5261,8255,8365,8422,7616,5651,7521,5237,8721,8479,8365,7811,7795,8073,5459,8222,7982,7972,8647,8282,8261,5301,8631,9026,5419,7542,5291,7864,7565,5407,5534,7742,8073,7786,0,7588,8093,5476,5255,7839,5309,7977,8255,7710,8527,8903,8249,8057,8863,7667,8411,7576,5655,8903,5485,7616,7706,5407,5573,8539,7742,8249,5622,5352,7516,8833,7478,7920,7883,5534,7616,7834,8811,5491,7914,7478,5606,7653,8227,8119,8577,8119,7877,7877,8222,8119,7588,8365,5218,8952,5329,5564,5218,5413,8527,8381,5606,5337,7977,7914,8261,7828,5296,7823,7828,5255,8805,5396,8805,7768,5209,8249,5358,8267,8365,8467,7511,8227,7715,8267,8010,5564,8845,5606,5381,5381,7768,7994,7696,5606,7696,7528,7533,7487,7834,7834,5391,7511,8543,5655,5573,7715,7790,8604,8307,8126,5358,8245,7972,5329,7706,5329,7768,7779,8010,5396,7628,7628,5237,7667,5476,7817,7628,5459,5442,5224,8903,8287,5325,5509,9382,7924,5407,8641,5666,5325,5496,7877,7592,8641,7516,8028,5419,5442,8461,5459,5459,7920,8077,5437,5320,5320,7972,5476,8519,8131,5693,5358,5381,8239,7817,5717,8484,7667,8789,5261,5651,8293,5301,7924,8033,5337,7904,5381,5568,7972,5564,5661,5358,5476,5301,7658,8519,8527,7605,8174,7920,8811,7990,7839,5661,8873,7883,5655,8048,7972,8909,8170,5325,7893,5726,7828,7977,5274,5496,5224,9032,5241,5218,5485,7768,5248,8316,5476,8509,7994,5296,7888,7924,5301,7972,8727,8387,5712,8307,5485,7675,8316,7516,8211,7920,7675,7940,7734,5301,7972,8451,8170,7478,8302,8149,8087,7811,5274,8527,5352,8527,8721,8772,7893,7538,7516,7710,5325,7828,8345,7710,5358,7930,8438,7786,8217,7864,7994,7628,8402,8479,7605,8504,7883,5501,7867,5325,7628,5730,8527,7986,7924,7752,5647,7628,7795,5396,7811,7692,7977,7482,5476,5329,5514,5291,7710,5237,8721,5476,7752,5325,8519,8282,5666,7511,7637,8087,8293,7828,8245,8959,8772,7877,8473,7795,8140,7632,5476,8548,5442,8015,8533,7972,9045,5699,5496,7957,5329,5305,5309,7482,7616,5464,7888,8239,7610,5485,7957,8149,5274,7893,7883,5529,7990,7834,8282,5496,8519,8625,5407,7628,7994,5476,7893,7920,5666,5301,5320,7883,8461,7828,8073,8277,8959,5325,7710,7924,5274,5381,5241,7834,5301,8154,8277,5407,7828,5274,7616,7482,5726,5218,8772,7828,7687,8509,8375,8073,7877,7952,8312,8048,8919,8048,8126,8126,7867,5358,5372,5372,7790,8407,5314,5381,5464,7952,7839,8093,5524,7952,5407,5285,5352,5524,7663,7864,8052,5407,5358,5509,8498,5305,5237,7663,7592,8052,5464,8052,7817,7977,7478,8349,8577,8312,8946,5285,7972,7582,7605,5301,8647,5726,5285,7920,8811,8174,7576,7667,8149,8879,5699,5568,7786,7667,7667,7576,7687,5381,8427,8762,8438,7990,5485,7628,7930,7999,7548,9054,5218,7616,7849,5476,8329,5676,7849,8608,8427,7834,5407,7849,7628,8005,7582,5671,8479,7533,7478,5688,8119,8479,5329,8427,8249,5329,5309,5391,5407,7533,8721,8345,5329,7924,7576,8903,8010,7786,5301,7715,7558,8811,7500,8211,8211,7972,7706,7935,5606,7521,5471,7528,5237,7533,8222,5471,7478,8267,8119,9389,8267,7746,7533,8827,7622,7883,5712,7790,8795,5274,7742,8345,8174,5241,5642,7914,8154,7723,5655,5396,7930,5386,7710,8119,5712,5476,7605,5655,5655,7746,8582,8467,5459,5476,5209,5209,8783,7687,8190,7893,7982,7853,5342,7952,5651,7883,5476,7696,8391,8190,7710,8582,5676,8711,5655,7681,7924,7834,5305,7548,8447,7999,5241,7528,7667,8670,7653,7811,8539,7478,5296,5676,7864,5509,5274,5237,8653,8277,5407,5261,5314,7643,5509,8093,7823,7883,8577,9394,5485,5241,8582,8277,5342,5676,8839,5342,8762,5329,5358,7715,7643,8447,5329,5476,5381,7774,7576,7628,7643,7482,7994,8670,5325,5699,8375,5391,5676,5661,8762,5309,5699,5377,8005,5524,7872,8711,7500,8048,7752,7622,5442,5301,5651,5534,5568,8387,8033,7990,5448,5367,7723,8334,8391,7800,8387,7972,8015,8334,7757,5301,5305,8598,8827,7957,7710,7800,8647,7558,5325,8249,7834,5301,8959,8302,8255,7734,7605,8919,7914,8598,8239,7487,5301,8795,7491,7500,8316,8711,7516,8005,5274,7516,7663,5296,8427,5407,7786,8762,5726,5622,8589,5514,8179,8329,8179,7663,8249,5337,8888,7746,5337,8582,5291,8190,7746,8154,8048,8893,7610,8721,7800,5726,5573,5629,8795,7605,7790,5309,5524,7628,5448,8604,7872,5391,5476,7786,7972,8093,7914,7616,8170,7972,5285,8451,8676,7663,5285,8833,8154,8126,5391,5688,8312,5325,5209,8811,7478,5480,7692,8322,5237,8010,8582,8903,7538,8302,7883,5717,5301,5564,5209,7849,8154,7757,8879,7706,8795,5381,5501,7864,8387,7920,5578,7834,5342,5622,7935,5471,5529,7834,7667,8527,8048,8068,7687,5726,7628,7632,5480,5381,8539,8533,5655,7663,7893,7715,7605,5301,8411,8293,5255,5352,8093,8174,5333,8312,8073,8884,7914,8925,7495,5301,7610,8316,7742,5301,5524,5573,7667,5329,7605,5717,8154,7663,5386,7977,5329,7994,5564,7632,8800,5426,8119,8316,8068,5237,5476,5491,8851,5367,8255,8467,7908,8255,8322,8946,7675,5699,7768,7843,7752,7628,9059,5367,8211,5337,8539,7935,7935,7864,7511,7616,8211,5699,7834,8302,8653,7628,7994,7500,7849,5386,5407,8456,8022,7675,8946,5352,8391,8863,8571,8554,7706,8925,8745,7516,7667,8073,5693,5564,7757,7849,7687,5329,7663,5342,5699,5564,7930,7576,8217,7478,7706,8099,5509,7828,8461,8456,7706,8033,7487,7834,5459,5693,8762,5480,5381,7616,8411,5476,5358,7994,8851,5655,8676,5367,8903,7576,5386,5352,5407,8539,8772,5496,7473,8359,8272,5274,8190,5699,5699,7538,7834,8184,5476,7663,7839,7914,7487,5629,7898,5396,8631,7994,8940,5666,8451,5655,5367,7706,5401,7533,7994,8245,7675,5578,7990,7986,5396,9399,8234,5622,7877,5325,5708,5352,5329,5329,8795,8322,8033,8211,5666,7864,7706,5305,7706,8402,8443,8745,8287,7675,5325,8149,8073,5726,8833,8919,7904,7763,7516,5342,7828,5464,7533,8925,8255,8539,5301,5337,5285,7786,7786,8255,5471,5612,7628,5476,7849,7977,5647,5666,7811,5573,5237,5699,8903,5476,5419,7920,5372,8387,8751,7706,7663,8196,5407,7628,8821,8249,9406,8149,7779,5612,7763,8936,5274,8582,5666,5666,7972,5301,5301,8010,5699,8087,7616,9045,7610,7696,5301,8073,8617,5381,9067,8365,7834,5391,5391,5301,8705,5617,8354,7487,7706,7738,8800,5661,5274,5274,7972,7706,7972,5413,8217,7548,8068,7478,5391,5337,7972,7681,5564,5564,5651,8443,7972,7491,5209,7571,5726,7982,7605,7914,5342,7839,7637,5651,5329,7628,7521,7986,7681,8647,7605,7952,5367,7719,5325,8077,5655,5524,7834,5309,8033,5671,8073,5693,8479,8154,8272,7706,7710,8721,8397,8484,5329,5224,5309,8479,7823,5329,5261,5578,7986,5296,5730,5237,7482,7576,8154,5655,5564,8467,7482,5274,7924,5377,5476,5712,7487,8539,5296,5314,5655,5218,5325,5325,8005,7487,7538,7738,7500,8190,8762,7628,5237,5325,7628,7877,8772,7908,5342,5301,9054,8447,8479,5301,8261,8354,7622,8365,5666,8110,7628,7628,7710,5381,8196,7849,5647,5519,7487,5642,5647,9051,5496,8811,7719,5274,5224,8062,5688,5476,5248,5501,8052,8370,7990,7622,8940,5606,5358,7972,8631,5651,8334,7478,5274,8447,5301,5337,8443,7920,7904,8631,5305,7558,8052,8196,7924,8370,7972,7972,5320,8170,5209,8154,5325,7558,8136,9075,7663,7849,8316,5209,7972,5381,5401,8762,5476,5305,5296,5448,8772,5237,7706,7843,5564,8115,8839,5329,8589,8811,7977,5377,7558,5647,8670,8839,7786,8099,5329,7786,7571,8504,9032,8115,8126,5325,7972,7972,8653,8873,7982,7990,5224,5442,5629,7628,7588,7616,9411,5476,5651,8411,5396,8349,5358,7571,5352,7548,8548,7500,7558,8479,7752,8397,7675,7487,5325,5564,5655,8255,8479,7977,5325,5524,5524,7786,7864,5342,8010,5325,5296,7715,5261,8514,7972,5325,5329,7588,5325,7616,7752,5688,7719,5401,7500,5464,5296,5325,5342,7675,8196,5285,5314,8316,7487,7834,5342,8789,7719,5296,8261,5309,8789,8354,5325,8422,7752,5372,8154,7817,5329,8473,7663,8261,5314,7908,7628,5529,8631,8484,7723,5237,7723,8571,7628,7920,5485,5642,8732,9032,5647,5347,7867,8533,5642,8411,8222,5642,5224,8631,7605,5285,8255,8052,7478,7952,5642,7972,8519,5642,7681,5285,8519,7592,7675,7482,7982,8498,8494,7883,7692,7990,9417,7500,7588,5305,8005,8307,5329,7482,8494,8631,8387,7628,8170,7723,5642,5642,7681,8783,7628,5606,7757,8170,8239,5237,7986,8005,7675,5285,8287,8739,7610,7558,8402,7675,5372,8498,8255,7637,8307,5372,7632,7867,5301,7588,5391,9423,7542,8925,8062,7706,7542,7700,5391,5320,7994,8170,8427,7605,7487,7610,5224,5485,5485,8015,5476,7487,7667,8062,8316,8022,7592,7858,8267,5320,5485,5296,5381,5255,8676,8670,8827,7994,5647,7994,8099,5485,8795,5485,7605,7605,5333,7658,7628,5647,8811,8211,7605,7643,8762,5325,7675,7904,8099,5325,5655,7999,7962,8732,7994,8467,7982,8461,7675,8433,7920,5358,5407,5647,5647,5261,8119,5367,8539,8467,8136,7710,5647,5301,5647,8438,8222,7681,7487,5564,8387,8543,7994,8245,5209,8539,8093,8115,8293,5564,8131,8010,7977,5717,8010,5391,9427,8312,8582,5407,7723,7610,5305,8411,7977,7952,7972,8282,8329,5642,5274,5358,7723,7795,8919,5285,5309,7768,7962,8613,8048,8582,7605,8316,5426,8245,5485,8604,7908,5568,5485,5358,8635,5301,5241,7478,5501,8267,5647,8994,5301,7843,8170,8721,5407,5480,8316,5255,7828,7558,7940,5391,7920,7571,7675,8670,8805,8604,7834,5524,7908,8811,5442,7904,8461,7935,8093,7542,5564,5407,7972,8277,7986,5358,5358,5358,5717,7710,8174,7628,5407,7999,7786,7710,5241,5485,8302,7576,5671,8073,5325,7516,7940,7667,5564,7687,7786,8571,7542,5496,7667,7805,8721,8467,8467,8170,5661,8115,7867,5480,7632,7883,7675,8893,5407,7473,8411,8272,7521,8509,9435,5655,8119,5426,5391,7605,8354,8456,7904,5476,7482,7675,7500,5381,5464,8845,7663,8010,7565,5622,7858,8329,7605,8062,7867,5730,8190,7622,7542,5309,8005,5666,7957,8267,7849,9441,8201,5442,5524,7893,8893,5241,8131,8077,7542,7675,8498,7817,8334,8762,5301,5358,5314,5573,8635,7710,7605,7582,7610,5347,8345,8201,5301,5296,5358,8539,7888,7834,5352,9367,5622,5325,8647,8307,7628,7763,5622,8115,5237,8039,5476,8447,7877,5568,8140,8461,5442,5325,7952,7588,7957,7687,5329,8184,5612,8498,8345,5612,7888,8676,5647,7558,8073,7643,5358,8903,8239,7883,7828,5564,5407,8438,8005,8539,8255,8919,5464,7962,8149,8365,7675,8762,5391,7864,5642,5213,8222,5325,8811,8227,8762,5480,7834,8073,8903,5381,7990,7610,7542,5401,8073,7576,8277,7521,5347,8873,7628,8354,8052,5655,7658,5377,7616,5442,5325,5218,8509,8909,7588,5391,8010,5448,5534,7558,5314,8909,5329,5329,5407,5480,7898,8159,8277,7610,5606,7478,5347,7542,8354,7548,5726,7817,8411,8676,8857,5301,7752,7632,7616,9447,7843,7500,5651,8539,8010,8140,5329,7914,8293,5661,5666,5274,5209,8447,8879,7710,7542,8473,5358,8282,7478,8539,8015,8110,5501,5358,8402,7972,5301,8039,8322,5309,7904,5301,7715,8170,8312,5305,8140,8184,8322,7779,7588,7681,5301,7706,7588,5261,8316,5209,8604,8354,7576,8010,9345,8126,5471,5501,5471,8539,8119,7779,5274,8772,7779,8179,8845,5301,8354,8245,7491,9452,8402,8126,8015,7516,8548,5485,8039,7920,8589,8589,5419,7491,8110,8073,5730,7500,8170,7982,5407,8365,7839,5301,5347,5564,8447,7930,8387,5519,5524,5629,5688,5666,8340,5329,7834,5651,8589,5501,5372,8756,7872,5305,7516,8261,9457,8329,7779,5419,7904,8073,5391,8504,8548,5274,7495,8641,8077,5301,7990,8539,9345,7681,5347,5651,5476,8484,5237,8653,5241,8484,9021,5325,5496,7576,8329,8354,8062,7628,7849,5509,8504,7752,7752,5688,8136,5209,7853,8816,8998,5381,5320,8104,7920,7482,8302,8062,7972,5325,5501,8267,8504,7883,5309,7588,7565,5612,8249,7482,8154,8033,5309,5564,7972,7658,5209,5442,5642,7687,5666,5237,5564,5661,7914,7853,7605,5726,5476,7605,8670,8811,7924,5564,5480,5655,5491,5314,5391,7548,5274,8443,7592,8282,8170,8716,8527,8397,5501,5485,8504,7946,5661,5381,7605,5442,7795,8126,5291,8582,8272,7839,8543,5237,8272,5661,7757,5285,7768,7828,7632,8190,5224,7533,5708,9462,8316,8164,7681,7482,5666,5419,7768,7500,5329,5459,8316,7487,5419,8293,5296,5291,7972,5407,5676,7710,7746,5655,5329,5274,5688,5320,7898,8613,7675,5509,8196,8845,5564,5377,5519,8504,5564,5237,5564,8805,5342,7548,8653,5564,5485,7982,7548,7834,5309,8267,5325,8456,5391,8548,7516,7888,5301,5329,7779,5442,7768,7628,5480,7706,9032,7565,5509,7542,5274,8087,7994,7706,8272,5509,8461,7599,8716,8863,7774,8190,8402,8099,7511,7706,8427,7930,8287,8093,7746,7706,7867,8772,5241,8234,7967,5476,5209,8093,7779,5309,7893,7872,5261,7628,8548,5381,5329,7500,5426,7994,5209,8170,7521,5407,7990,5274,8821,7746,7706,8676,7548,8539,8438,5676,8467,5661,7715,7487,5688,8411,7811,8845,5325,7746,5274,5564,7994,7628,8131,7622,5661,5401,8456,8411,8267,7834,5377,7795,7972,7710,7952,7768,5209,8022,8467,8062,7500,7957,8387,7648,5285,7616,5309,5476,8863,8447,7898,7786,7533,7576,5407,8451,7768,8617,8267,5367,8952,8929,5426,8272,5564,8267,5655,5524,8613,7982,5437,8909,5274,7588,5655,8548,8422,7653,7774,5358,8527,7478,5301,5612,7482,5237,8548,5666,5622,7542,7800,7972,5329,5305,8647,5401,5651,8190,8548,5325,5261,5342,8010,8952,5413,5476,5514,8711,7576,8322,7643,8473,5237,5320,5519,5296,5564,5296,8255,8833,7571,7972,5261,8589,7516,7628,8010,8239,5485,9021,5407,7648,7877,7872,5519,5296,5396,5442,8287,8322,5485,8245,8119,5296,5237,7482,5407,8170,7811,5372,5274,5381,5285,8073,7877,5476,5386,5342,5476,8635,7500,5325,8888,7542,5347,7734,8329,8857,7706,5712,7883,5305,5666,5661,8857,8582,5337,7500,5666,5274,7877,5237,5329,7877,7616,7774,7521,5274,8397,8857,7632,5237,8479,5347,5688,5377,7487,8484,7628,8062,7576,5274,7849,8354,5325,5509,8582,5325,8504,7994,8267,7853,8104,7920,8302,8461,8136,8467,8548,8099,5501,7972,7752,7565,7533,5708,8816,5320,5274,8811,5661,5564,5401,8443,7914,5237,7972,7548,7576,8282,5564,5314,8670,5476,5485,5491,7898,8164,5381,7692,7648,8647,7946,5296,9462,8190,5419,7632,5291,5285,8316,8845,7675,5676,5519,5309,5342,8272,5325,5377,5325,7500,8093,5578,7500,7706,7893,8287,8015,8234,8119,7994,7746,8087,5413,5442,7790,7511,7768,8548,8427,5509,5329,8863,5209,7990,7521,5407,7746,5347,5377,7622,7706,7834,5296,7487,7888,8676,8411,5274,8131,8863,7811,8539,5329,8411,8438,8467,5309,5564,8613,5655,8527,7582,5524,7877,5301,5476,5407,8888,8647,7800,5666,7616,5612,8711,5514,5661,8239,5261,8833,5419,7542,5372,7811,5285,8857,8998,7538,8461,8998,5699,8762,7511,8073,5255,5730,8617,8093,7616,7616,8745,5367,7616,8617,8772,7768,7706,5534,7616,5666,8772,5329,8721,7599,5291,5291,7599,7653,5209,7632,5564,7962,8048,7786,7605,8170,7790,7786,8438,8461,7904,7795,7786,5676,5564,7516,7834,7999,8397,7795,5314,7628,7972,7542,8048,7823,5320,7487,7920,7538,7511,7904,8048,5301,8783,7994,5358,5325,8833,8316,7746,5325,5676,5651,7952,7632,7920,5325,5325,8375,7924,7763,8322,7738,5712,8062,5476,5337,5501,5651,5524,7924,7786,8548,8721,7576,8093,5476,7605,8539,5519,7542,5476,5274,8164,8658,8658,7542,7811,8397,5274,5476,5274,7817,5274,7628,8845,5381,5629,8467,5237,8010,7834,5501,5261,5401,5666,8845,8417,8322,7877,7628,7817,5476,5248,5534,5501,8062,5325,7710,8402,5564,8745,9054,5476,7986,5476,7920,8093,7511,7972,5726,5237,7628,7521,5237,8196,7706,8255,7628,8222,7675,7719,8196,5419,8539,8402,7893,8272,8302,7478,7605,5407,7605,5391,7858,8548,5509,5296,5730,7849,7849,7653,5708,8647,5476,5401,5726,8222,7516,9054,8255,7487,8010,5476,5296,8190,8479,5642,8149,5622,8898,7790,5578,7706,8131,5622,7994,9021,5459,7696,9045,8821,7696,8716,5622,5666,7696,5209,8845,8783,8052,8267,8994,8987,7710,7487,8227,7977,8170,7849,5573,8149,7599,5325,5629,7742,7957,8417,8131,8334,7599,7957,5642,7888,8005,5337,5305,8811,7696,5578,8805,7491,8245,8022,8196,7710,7710,8447,7883,8062,8845,8170,7920,8077,7883,5358,8170,8093,5241,5337,5377,8239,7610,7972,8381,5391,7696,8387,5459,7795,5237,5401,8427,8119,8577,8751,7864,8170,5255,8539,8039,7752,7500,5647,8827,5329,5237,8805,5329,8170,8005,8170,8005,8827,5291,5534,5237,7935,5401,8827,5708,8119,8022,8805,8005,8077,7972,7883,8381,7706,8504,7478,8805,7610,7628,7628,5342,7487,7588,5476,7588,7487,7999,8387,8110,5476,5333,5325,7542,5501,8340,5717,8433,7811,5464,5496,8504,7675,8461,7576,8467,8762,8461,8164,8811,7571,8494,8227,7786,7972,7653,5564,7893,5301,7681,8170,5407,7565,9032,5337,8136,8560,8800,5337,7817,7628,8354,5642,5309,8312,7542,5237,7681,7893,7710,5485,7658,7914,7548,8015,5661,7914,7994,7576,5459,7883,7893,7904,8073,5726,8329,7930,7795,7658,5209,7692,7972,8316,5476,5209,5564,8334,5655,5274,7571,8456,7977,8119,7972,8222,5337,5726,7982,5301,5476,7482,7500,7924,7610,7582,9012,5329,5485,8293,7834,8658,7473,5476,8687,5671,5717,7582,7972,7675,7914,7504,7696,7864,5426,7858,8766,5209,7628,7558,7681,7972,8277,5274,5661,8402,5661,5305,8349,5325,5396,8170,7908,7730,8205,7706,5386,5651,8789,8375,5509,8073,8461,8721,5688,5285,5485,7888,7946,8451,8577,7582,8805,5712,8104,7914,5661,7834,8397,5381,5519,8443,5480,8174,7558,7786,5564,8307,5480,5453,7628,7994,7924,7795,5688,5401,8015,8402,7972,5391,7576,7920,7864,7605,8795,5329,8005,7805,8811,5606,8028,5485,8805,7986,5476,8345,8504,7914,8287,5564,5564,8170,7893,8322,5509,5496,5342,8164,8548,5693,7478,5712,7957,5622,7972,8694,7795,7742,8903,8345,5426,5386,7628,7478,7823,8255,7548,7994,7864,8159,5647,5642,5407,7994,5285,7628,7977,8676,5358,8227,5564,5274,8447,7746,8631,8909,8422,7898,7957,5329,8631,8427,5708,7588,8863,5717,5476,8073,8239,5325,7920,7628,5524,7610,8888,8554,8227,8196,8039,7834,8663,8647,5419,5564,5629,5305,5237,5622,5329,5285,7952,5329,7972,5642,5291,5401,8316,5352,8447,7687,8239,8402,7864,7675,5325,8010,7972,5285,5261,7478,8402,8489,7521,5342,5501,7616,8772,9361,7883,7908,8789,8068,5401,5358,5218,5291,5573,5476,5676,7986,5401,5514,8005,7786,7706,5386,8888,8329,8613,8154,8205,7828,8473,7834,8739,7986,7834,5329,8154,7952,7632,5726,8205,8375,8447,8205,8033,8577,8494,8687,8205,7516,8687,5209,7478,8077,5442,5325,5688,5688,5325,8077,7972,7972,7972,7999,5459,7972,5519,7658,5655,8484,8329,7628,5496,5642,7628,5248,8201,5237,8484,7628,7628,5426,8170,8484,7528,5617,5617,7883,7994,5391,8772,5320,7538,5606,7883,7734,7734,8721,7972,7576,8732,8598,5367,7478,7576,8479,8461,5261,5476,8613,8994,5491,5241,8282,8732,8560,7565,8282,5699,7898,7548,5407,8777,8093,7605,8115,7888,8073,5274,5717,8732,7977,5329,8062,8334,5237,5730,5564,7500,8527,7994,8670,7628,5476,8805,5255,7994,7817,8249,5464,8479,5464,8087,8548,8411,7790,5459,7800,7898,5496,5476,5514,8777,8340,7957,5491,7877,5642,8316,7982,8903,8772,5329,5476,5381,5407,8255,8249,7706,5329,5329,7877,8617,8682,7779,5407,5407,7719,7834,5564,5255,7834,7834,8340,8340,5476,7811,8805,5688,7663,8716,7681,8805,5381,5352,5509,5476,8745,5358,7763,5471,7542,7628,8387,8851,7723,5647,5367,7706,8211,5209,5296,8539,8174,7521,5358,5564,8068,7994,7478,7972,7706,5261,7752,7542,8504,8998,5568,5381,5329,7500,5325,8196,7706,8467,5501,7904,7872,7528,5325,5396,8277,7565,7478,5291,8131,7972,8267,8196,5708,5573,7790,8387,7893,7622,8560,8827,7482,5337,8411,5476,5296,7994,5301,5401,7864,5274,8227,8777,8249,7588,8484,8467,8297,8093,7658,7681,8539,7622,7723,5301,5573,7990,7715,7487,8222,8131,8438,5476,7893,8170,7920,8893,7692,7605,8447,7516,5224,5261,5688,5642,7616,8543,8062,7616,7687,7582,8170,5459,8077,8811,8539,5255,5712,5564,8716,5296,5358,8184,5564,5314,7790,8010,7867,7790,7790,7904,5209,5305,5358,5476,7710,5325,5485,7610,7696,5453,5391,7817,7616,7723,5717,8391,5301,8164,7482,8682,7542,7904,8033,8539,5519,5291,8577,7478,5325,8005,8604,5248,7839,8131,8687,8211,7710,5285,7768,5564,8670,7605,5209,5419,5296,7482,5496,8048,8164,8613,5248,8211,8316,5617,7883,7605,8329,7811,7473,7768,5671,8245,5485,5426,7768,8340,5485,7696,8345,8087,7710,8504,5407,5377,5464,5255,5301,5329,8149,5296,9345,5296,7977,7648,5358,5476,8255,5296,7706,8903,5426,8164,7643,7616,7632,7616,8329,5309,7962,7482,8349,8062,7576,5642,8015,7616,7710,8307,5564,8504,7482,7849,5358,5476,8267,8539,5647,5342,5342,7558,7599,7478,8140,5237,5485,5301,5564,7605,5305,5301,9345,5325,7888,5688,8302,5612,8033,5485,8170,5712,8249,5426,5485,7935,7528,7667,7834,8805,7757,8451,5509,7653,5337,7628,7977,7599,5337,8816,8670,5274,7843,5564,7864,5305,8334,5642,7823,7715,8795,8277,5401,5391,8494,5241,5401,8154,5337,7616,7616,7994,8104,8211,5712,7839,5509,5485,8539,5726,7582,5407,7967,7786,7986,5329,8170,7994,7667,8504,8653,7849,8772,5209,8594,8039,5564,8099,8227,5442,8022,7805,5237,8514,7774,7904,5301,5647,8073,7653,8222,8196,5676,5237,7491,8539,5381,7734,8903,5309,7999,8411,8845,5396,7786,8093,7893,5309,8345,7599,5413,7576,7834,8170,5407,7516,8772,5320,5606,8479,8359,7706,8022,8402,8272,8227,7930,8227,5476,7893,8277,7706,8249,5255,7994,7883,8302,7811,7834,5476,5241,8994,8201,8964,8716,7982,8170,8484,8964,7632,5442,8539,8249,5476,5647,5337,7616,7616,7616,7675,7730,5688,7990,7521,5509,7628,5337,5564,5401,8190,8467,7823,8115,8494,8104,7675,7628,7858,7790,8345,7864,7632,9467,8154,7795,8504,7746,7571,7571,8170,8222,8110,7715,5661,5661,5726,5407,5564,5329,8052,9054,8527,8062,5237,5730,5320,5647,7605,5688,7521,7914,5717,5381,5485,5651,5464,7914,7734,5671,7582,7500,7542,7994,8073,7994,8994,7990,7982,8205,7977,8676,5642,7667,8827,5352,7994,5274,8005,8805,7643,5237,5391,7706,8604,5386,5642,8467,5218,5218,8467,8539,8653,8721,8354,5529,7877,8509,8005,5578,5419,7957,7994,5213,7867,5448,7482,5476,7542,5391,5237,7930,7843,8293,8249,7849,5699,7940,8467,5301,8170,5237,8334,5573,7811,5647,5476,8354,5655,7542,8443,8312,8451,8467,5573,7628,7908,7533,7734,7734,7888,8851,8227,5501,5301,5241,5407,5325,8893,8777,8869,8571,8052,7962,5655,8391,8653,7500,7511,5237,7786,8039,8164,5534,5671,8451,5391,5629,7632,8613,5612,5274,7930,5642,5377,5358,8548,7599,8287,5519,8604,5237,5347,7528,5325,5301,7864,7872,7628,5407,7616,5305,7757,7681,7790,8316,7605,5352,8179,5401,8827,5237,5255,5688,5309,8104,5459,7994,8422,7723,7628,5476,5325,8239,5358,5329,7588,8201,5442,7632,8190,7994,5717,8093,5325,8647,8234,7653,8539,7588,5329,5617,8987,7746,7763,8329,7800,8073,8539,8451,5578,9367,8190,7994,7663,5708,5651,7957,8140,7548,7774,8159,5419,5651,8827,5453,5296,7667,7482,5407,7834,5320,7482,8005,8033,7982,8302,7786,8170,8519,8205,5661,8322,8307,5401,7516,5568,7663,5358,5301,8467,5325,8329,5218,8010,8249,7742,7877,7828,8329,5285,7872,7872,5464,7628,8255,8391,5396,5407,8827,8267,8222,5337,5726,5329,7648,5296,7864,8909,8438,5291,8397,8316,8613,5501,8005,8598,7872,8387,7500,5564,5291,8903,7478,8316,7487,7675,5237,8987,7994,8641,7706,7542,8267,7962,8093,7628,5367,5218,5325,8062,7487,7706,5666,5255,8267,7628,5237,8052,8073,8052,7663,7994,5296,7994,5325,7839,7521,8375,7763,5476,8873,5419,8005,5296,8322,5401,7990,8467,8245,5377,5629,5661,5358,8427,7962,5401,8010,5693,5381,7786,5661,8149,5407,8898,8062,7730,7528,8329,5642,8239,5448,5320,8329,8217,5274,7994,8766,5442,8349,8613,8647,7658,7706,8772,5485,5655,7696,8447,7616,5524,8827,7675,7706,8048,5407,7500,8005,5381,5708,7757,5476,5347,5309,8777,7622,7723,7867,5274,7516,7742,7795,7588,5329,5476,7500,5491,5301,5476,7986,5666,7706,8539,7972,7930,8909,5642,5305,7495,8647,8010,5448,8772,8422,7723,5337,7994,7511,5296,7558,5524,7877,8249,5301,5320,5573,7663,7516,5372,8170,8033,8277,8154,5480,7478,7872,5629,7706,7946,5329,7588,5717,8211,8267,8751,7628,5320,8164,5237,7558,5305,5381,7588,5407,8322,8077,8073,8261,5476,5274,7628,8170,9353,5401,7482,8322,7616,7610,8316,7994,8354,8093,5726,5717,8010,5651,7628,8005,5347,5325,8903,8427,5666,5666,5396,8527,7972,7994,7982,8427,8359,5301,5325,5651,7982,8359,7632,5325,8411,7828,5407,8987,5476,5666,7768,7675,7893,7834,8514,5432,7982,7982,5329,8293,5329,7487,7675,7542,5573,7982,5437,5480,5564,7500,7500,5329,7692,7628,8052,5496,8170,8039,8222,7834,8427,7542,8005,8282,5237,5320,5248,5688,7757,5325,5688,7558,5391,7542,8539,5578,7628,5564,8811,5237,8062,5296,8249,5717,5342,5367,7637,5391,5476,7538,8411,8783,7864,8994,7710,8354,5291,7752,7872,7898,5358,8354,5509,7667,7706,5320,7482,7706,5606,8467,5291,5296,8196,7706,8174,8387,7839,7715,8641,5568,5501,8560,7817,5372,5564,5224,5381,5564,7710,8307,8647,5274,5642,5655,5688,5337,7687,7849,7710,5337,7849,7653,8170,5301,5564,8297,7687,8131,7920,8073,5274,7605,7757,5432,5224,7786,8297,5726,5617,8964,8093,7786,7883,7786,7616,5301,7558,5726,8711,8170,7839,5296,5480,8211,5564,7924,8631,8577,7605,5476,5274,8144,5285,8170,8144,5255,5325,5201,5218,5476,8381,5305,7723,7867,7952,7817,7972,7800,5241,7972,7977,7648,7487,7706,5426,8577,5688,8670,5218,7977,8839,8751,5635,7864,8136,5320,8190,8527,7914,5655,5564,5274,5578,7478,5564,5301,7843,5358,5573,8467,7582,8613,7883,7834,8805,5329,8839,8631,7795,7972,8022,7893,8170,5651,9361,7706,5661,7667,7687,8249,8322,7487,5237,8099,5301,5459,7681,8272,8451,8811,7653,7628,5655,7893,7920,7994,5471,5476,8467,8170,7768,7542,5476,8447,8745,7994,5509,8039,5509,7582,7864,8282,8539,8484,8316,7924,7616,8039,7877,8057,7986,7990,5325,7849,8805,8349,7500,7746,7628,5381,5391,8190,5464,7834,8411,8110,7994,5564,8467,5712,7994,5564,8821,5661,5337,5688,7790,5367,7616,5391,5248,5629,7605,8548,5377,5564,8467,5296,7663,5676,8762,7982,5688,5459,5301,7795,8267,8170,7823,7957,8658,8164,7616,5274,7558,7843,8334,7487,5237,8461,5237,8467,5534,8057,5448,8015,8164,5325,5325,7478,5688,7653,8196,5476,8093,5396,5629,5476,5241,8716,7849,7982,5296,7538,8391,9471,7774,7616,8909,7628,5381,5377,8571,7800,7834,8987,5578,8340,7710,8539,5708,5329,8087,7972,5629,8033,8647,5726,7920,7628,5407,7632,7957,8196,7548,7482,5419,8952,5655,5285,5509,7972,5578,7538,7538,8039,7511,7790,5237,8827,7967,8190,7622,5377,8015,8211,7521,5305,5655,5476,5688,8594,7972,7924,5352,5285,5726,5432,8239,7533,7952,5237,5325,5622,5464,7893,8149,5291,8509,5296,7482,7877,5285,5261,7648,5248,8851,5248,8329,7487,7542,7558,8903,7687,5661,5261,5476,5485,5347,7877,7706,7839,7521,5617,7632,8845,7616,7637,8196,8115,8190,5401,7605,8115,5661,8211,5661,7706,8164,5413,8249,8052,8821,8154,5314,7972,8888,5661,7763,8010,5666,5524,7986,5651,5301,8104,5301,5688,8077,5329,8762,7675,7605,7610,5301,7610,8245,7491,5413,8987,7786,5676,7616,7542,5237,8179,5726,7768,8762,5237,7800,5291,5381,7605,7687,5661,5464,8816,8456,8293,7528,7548,5642,7696,5448,7763,5237,5426,7834,8795,5248,8519,8039,8190,5320,5274,8316,7610,5381,8533,8387,7982,7994,8721,5248,7994,7957,8548,7946,5726,5476,5476,8201,5396,7628,8473,5496,5476,5261,8033,5564,8234,5501,8952,5381,8136,5568,7883,8915,7548,7548,5305,7706,5578,5274,7972,8504,8196,5309,7667,8467,5712,7637,5708,5453,8959,8959,7977,5480,5274,5491,8222,8174,5642,8329,7558,8670,8772,8170,7946,5320,5519,5655,5476,8994,8073,8539,8631,5296,8234,7972,5325,8297,7533,7883,7790,8073,7935,5568,5274,8239,5726,7930,8234,5237,8451,5314,5622,5564,5476,8196,7692,8504,5419,7977,8879,5305,5381,7738,7632,8613,7605,7952,7482,7774,8577,8144,5329,9345,8903,8115,7734,8800,5453,5320,7977,8653,7558,5301,5676,5381,8057,7908,5476,7628,5655,7511,5688,8658,5325,5476,8201,9345,8839,8302,5325,5301,5401,5401,7692,5407,8936,7877,5358,8613,7491,8467,8093,7982,8227,5442,7888,8473,7779,8227,8456,5305,7893,8467,7632,8039,7834,8762,5237,7972,8170,7972,7533,7972,7795,7730,5320,5377,5377,5564,7667,5661,8411,5459,8653,7616,5519,7610,7849,8467,8222,8222,5564,7628,8641,7834,5296,7930,5573,5329,5237,5617,8267,8245,5651,8245,8359,8164,8772,8613,8716,7592,8631,8282,8068,7710,8940,5453,7616,5381,7542,8359,7692,5401,8222,5301,5578,5391,5329,5671,5476,5661,5274,7994,8473,5241,8073,5419,9367,8222,7521,7786,7734,8604,7675,5407,5573,5325,7883,8925,5285,8073,7521,7528,5432,7648,7628,7710,5708,7888,7952,8340,5342,8789,7877,5401,5358,5309,8903,5320,8772,5358,5347,7834,8613,7706,8307,5320,8631,5391,8126,5661,8073,8033,5320,7610,5237,5274,5347,8539,5491,5564,5320,5391,5407,7533,5476,5285,8387,5237,5314,5642,8527,8772,5301,8473,5564,7904,7849,7706,5325,5407,8888,5291,8533,7952,7893,8282,8282,5651,7628,7763,5391,7487,8427,8239,5291,5651,7681,5401,5642,7972,5285,5237,5261,7994,5237,7710,7628,5347,8322,8762,8196,5314,5325,8391,7616,8131,7528,5314,5309,8721,7516,5314,7632,5726,7605,7994,5291,8560,7628,5347,7914,8539,8721,5407,8721,8427,7952,5651,5391,8427,5347,5291,5325,5237,8539,5485,7628,8131,5309,5726,5285,8484,5309,8140,5291,7706,7972,5325,7786,7883,8397,8539,8987,5396,7768,5717,7710,5381,7952,7817,8062,5381,7643,5688,5661,7588,8447,5325,7500,7972,7687,5358,5426,7982,8987,7877,7972,5529,5573,5391,5476,5329,5291,8068,7834,5237,7834,7930,5693,5564,5688,5381,8653,8062,5325,7565,8329,7952,8062,8190,8272,7500,5381,5476,8504,8329,5237,5261,7706,5480,5325,8772,7914,7864,7924,8387,5642,8022,7616,8190,7528,5291,8093,8884,8052,5337,7571,5305,7565,7500,8783,5617,8548,5391,8068,7500,7893,5476,7588,8447,7849,7752,7582,7500,5655,7605,5241,7610,5209,5209,8427,7786,5391,5464,5459,8093,8539,7500,5301,5309,7533,5309,8811,5476,8170,5708,7687,5480,5476,7675,7999,5255,5358,7504,5726,7904,5509,8443,8653,7576,7839,7558,5218,7500,8329,5476,7478,7653,5391,8052,5285,5391,5606,5496,8131,7952,7768,5485,5381,5396,5671,7521,7994,7914,5442,8316,5655,8427,8391,7864,5296,7516,7779,7795,7599,5413,7681,7710,5666,7605,8582,7473,8267,7628,8909,5325,7972,8504,7616,7843,7994,8582,7864,5391,7823,7883,8073,8345,5337,7946,8255,5612,5309,8391,7628,8509,7558,5381,8227,7478,7478,5524,8022,7734,5296,7952,8851,5655,8851,8762,7715,5305,8839,8447,7990,5296,8222,7653,5342,8732,8190,7893,7710,7738,5476,8136,7482,7757,7920,5325,7877,8033,9032,5325,7706,8772,8863,7994,7893,8745,5407,5241,5476,7967,5426,5314,7779,5407,7605,8052,8099,7914,8473,8359,7858,7628,5459,7500,7858,5391,8190,5391,7643,7834,7864,5329,8539,5661,5564,7700,7632,5407,8635,7628,5209,8732,5237,5296,7482,8345,5274,7823,8033,8527,7605,8340,7834,7946,8190,8554,7495,7706,8711,7491,7994,5296,8909,7967,5261,5407,8073,5337,5464,8154,5717,7616,5651,7675,5651,5237,8322,8033,8293,5377,7588,7898,7710,7542,5391,5419,8467,5661,5529,7706,5642,5642,7599,7628,8473,8827,8745,5301,5305,8919,8987,5578,9476,7687,8115,5381,5401,7957,8504,7742,8312,5237,5314,5708,5301,5309,5578,5476,7982,5419,7800,7839,7653,5396,7877,7768,7542,7719,5237,5617,8217,5241,5386,5347,7864,5209,5291,5325,9021,5285,8073,5296,5622,7715,8982,7687,5391,8255,8772,8982,7734,8119,7706,5372,8772,7681,9481,7542,9487,5285,5485,5401,8417,7538,7663,8427,7920,5296,7893,5476,5476,5261,8261,5642,7542,5573,8658,7982,7576,8052,7491,7616,7605,7628,5407,5296,8762,5325,8190,8365,5666,7914,7994,7920,5301,7599,8411,5578,7834,5476,5442,7823,5442,7982,7877,8261,7482,5347,8033,7516,5578,7811,8365,7999,8903,7972,7834,8762,7834,5325,5693,5564,8504,5325,5381,8062,8272,8261,5261,7706,7823,8329,5476,8329,5325,5476,5419,5296,7742,8170,8387,8884,7500,7864,8099,5476,8022,8527,5617,7565,5291,5241,7786,7849,5255,8443,5391,7849,7687,7588,5655,8539,7967,7839,5726,7999,8711,5476,5476,8582,5666,5391,7795,5391,5381,5296,7558,7605,7914,5396,7883,8131,7768,7952,8329,5476,5241,5671,7521,5496,7643,7500,5708,5381,7478,7628,8255,7478,5524,5337,5296,8582,8851,8022,7663,8745,7605,7967,7495,8762,5314,7893,7990,7994,7628,8839,7914,7757,7877,7779,9032,5342,8772,5381,5476,8711,8033,5459,5464,5407,5661,8293,7491,7994,7864,7628,8190,7877,5209,8345,7834,8539,5651,7858,8795,5305,8903,7588,5661,5642,7898,5391,7542,5651,5309,7800,5578,5407,9476,8772,8982,8255,5347,7734,5291,7834,7542,8427,8033,7786,7849,8062,5367,5291,8062,7653,8062,5237,5381,8154,8239,7653,8687,7610,8307,5419,7706,5391,5301,8919,5509,5358,5320,5274,8527,8940,8940,7811,7834,5391,5573,5296,8863,7628,8068,9494,5407,7904,5730,7864,8387,7521,7811,8903,8800,5671,5726,7908,5237,8888,8052,7628,7605,7977,7930,5237,8073,7752,7811,8888,5564,8548,5501,5476,7972,8222,7637,7653,7920,8136,7667,8811,8136,5337,7757,8805,7487,5519,7811,7610,5352,7849,5305,8190,7790,5301,8447,5358,7628,7977,8641,7790,7920,5237,7930,8170,5305,5573,8772,7495,5291,7930,5655,7675,5564,7774,7817,8594,5476,8560,8249,8751,8154,8010,8772,8721,8115,7972,7696,7605,5391,8653,5209,7696,7952,7858,8411,8349,5491,8119,8582,7893,8964,5476,7710,7790,7849,5358,5329,8504,5296,5671,9032,5209,7977,7746,9353,7706,5671,8205,5407,7616,7893,5655,5617,8888,7643,5688,7558,8732,5325,7576,8277,8365,7487,8033,8987,5606,8456,8845,5274,8349,5407,5342,5564,8772,8827,8467,7972,7628,8015,8762,5309,8504,8598,8267,7706,5437,8277,8527,8277,7967,8119,7663,5622,5564,8131,8159,8115,8077,7994,8039,8033,8052,7972,8863,8473,8267,8833,8772,8365,7834,8322,5261,8863,7616,5309,7930,5285,8205,5358,8762,5320,7834,5573,8190,9501,8811,5407,7930,5655,5564,7542,7817,7774,8115,5274,8126,8015,8154,7972,8010,8772,8751,8411,8131,7605,8721,7696,5407,5358,5391,8964,7952,8582,5491,7790,7893,7849,5476,9353,7972,7977,5296,5209,8504,5329,8205,5209,5671,5309,5688,5655,8277,5617,5564,8762,5325,5320,8987,7487,8005,7487,8845,8456,8772,8277,5407,8863,5564,5622,8159,8115,7834,8365,8805,8833,5261,7616,8811,7710,8190,5617,7823,7504,5237,7491,7849,7558,5578,5367,5629,7967,7967,5301,8484,5564,7999,8811,7706,5261,7723,8110,5274,7957,5291,9509,9516,7839,8033,5671,8772,8170,8783,7628,8033,5329,7605,8354,5726,5381,7710,5573,7795,5529,7839,7681,7888,8577,8811,5612,5407,8170,7999,5642,7482,8170,8302,8467,5629,8287,8402,7977,5726,5381,5688,8811,5377,5726,7542,5464,8255,8255,5642,5372,5377,7972,8365,8365,8443,8994,8222,7692,7990,5471,5291,5564,7738,5407,5237,7972,7730,8898,8994,7706,8354,8322,5480,7658,5476,8184,5325,8851,7904,5459,7692,5480,7977,7839,8174,7500,7605,5524,5459,7972,5564,7533,8170,5261,8073,8447,8170,5301,8682,5476,5325,5491,5274,5285,5296,7605,5622,7473,5635,7952,7723,8005,8140,7710,5329,5413,7972,7977,7681,7774,5407,8005,8255,5358,5274,7888,5325,8170,5325,5320,5647,5573,9345,8077,7675,8073,7710,8658,8711,5320,7843,5218,8316,5442,8255,5612,7738,5407,7706,8494,7994,5314,5381,5325,8077,8170,8170,8099,5329,8873,8359,8170,7710,5509,7768,5377,8073,8443,7867,5325,8140,5237,7774,8548,5352,7972,5241,7828,7972,7628,7888,7696,8104,5218,7548,8539,8827,8033,8663,5407,8005,5629,8170,5564,8443,5459,7858,8110,8048,5377,5496,7478,5407,8479,8451,5237,7738,8322,8427,7828,8427,5325,8077,7800,7511,8427,5358,5248,5329,8170,8140,5629,5491,5612,5237,8005,5325,7972,8322,8539,7800,8548,5401,5413,5372,5501,5432,8422,7946,8919,8554,7511,8010,5573,5573,5471,8073,8140,8589,5291,8307,7516,5329,8255,8255,7734,7738,5320,8498,8772,5372,7738,7482,5358,7605,8170,5573,8255,5471,5442,5325,7478,8354,8184,7516,8577,7972,5301,7972,7877,5573,8676,7994,7610,7516,5274,9361,8857,5347,5661,8261,5661,8077,5407,5726,5381,8498,8857,8022,8005,5291,7516,7914,7817,7482,8272,5726,5377,7888,7888,8888,8154,5325,7542,5651,8381,5476,5629,5407,7482,7834,8745,7834,5726,5651,8149,7952,8104,5325,5377,8312,8919,7628,7935,8387,7952,7667,7994,8354,5666,8539,8190,8010,8504,8498,5329,5320,8068,7742,5329,7864,8136,7706,7790,8821,8504,8010,5622,8062,9525,5726,5464,8539,8093,5491,5301,5655,8010,5381,8608,5352,8359,7786,7628,7610,7605,8164,7864,7904,7576,7972,5642,7990,5476,8745,5391,5255,7696,7723,8170,5285,5347,5329,8509,7768,7576,5437,7972,5617,7779,7628,8670,7632,7811,8267,5578,5509,5564,8745,7576,8447,8093,5578,8427,7779,5209,5296,7599,8287,8039,7839,7811,5352,7542,8234,8184,7742,7500,7930,8099,7710,7779,5476,8110,5325,7500,5367,8732,8694,8658,5717,5337,8062,5320,5564,7706,5564,7710,8196,8427,7628,7752,8795,7786,5655,5367,8467,5329,8484,8783,5622,7675,7643,5717,7935,5285,8427,7734,8149,5501,8010,7516,5717,7719,8795,7811,5476,5325,8217,7696,7628,5274,8062,5666,8456,5301,5301,5329,8201,7935,8387,7952,8190,8082,8504,5622,7864,8099,8821,8062,7692,7990,5329,7576,7972,5476,7982,5655,5301,5642,8745,5391,5437,7972,5285,5642,7768,5329,5285,5578,5564,7779,8184,7930,5296,8234,7599,5291,8287,5209,8694,5564,5325,8267,5320,8456,8467,5367,8427,5329,7935,7516,7528,7786,5655,7839,7920,5442,8888,5377,7986,8987,7482,8282,8411,8411,8411,5476,8411,8762,7839,8411,8411,8033,8613,8789,5320,5655,8676,8287,7482,8745,8411,8411,5699,7990,8239,8245,7877,5296,7542,7628,5699,7706,8329,5476,8039,8349,7706,7675,7599,8721,9032,7616,5712,8267,5309,8560,8387,8010,7478,8245,7864,7811,5578,7877,5309,8467,7994,5401,7710,7935,8447,7681,7972,5476,7592,7977,8164,5491,7696,8539,8312,5688,5459,8115,8272,5320,8170,7888,5485,5296,8316,5337,5651,5413,5325,8716,5476,5381,8863,8577,8329,8422,8022,7795,8272,8334,5730,8447,7632,8427,8190,8329,8201,5337,7675,5688,7853,7605,5688,8539,8539,5647,5485,5377,8307,8479,5578,8316,8687,7478,5391,7558,7588,5372,8658,8527,5401,5612,5237,5301,7924,8345,8387,7990,8745,8387,7811,5491,5329,8745,7576,5309,8267,8287,8005,7994,5224,5476,8245,9531,7632,5358,7495,7628,8903,8805,5471,5342,8888,8732,7710,8498,7628,7757,5655,5337,8267,8732,8411,8039,7864,5367,5464,5688,8479,8245,7994,5213,5476,8131,5642,7478,7511,8329,7738,8539,5419,8467,7588,7738,7542,5629,5642,7710,8015,5708,5391,7599,7710,5485,5372,5578,5471,8539,7768,7710,8451,7516,9021,7648,7542,8190,7628,5476,7478,8239,8170,5391,5358,5642,8349,5661,5342,7811,8427,8909,7696,7622,8411,8411,7986,5471,8467,8154,7632,5301,5476,5712,8919,7706,5218,5248,7653,7628,8211,7972,7723,7533,8582,5564,8756,8888,7565,5426,8136,7742,7914,7898,8467,8879,7994,5661,5305,7478,5320,5337,7653,7658,5688,5358,8456,5224,8670,7930,8048,8539,7967,7491,7516,7999,7952,7605,5391,7487,7628,7592,5717,5237,7972,5476,8329,5730,7768,5285,5237,5651,8543,7843,7977,7908,5241,7786,8267,7834,7558,8307,7710,5329,5564,7478,5564,5524,8052,8381,7977,8845,7667,7616,8073,5237,7930,5301,8267,8201,7675,7706,7920,8745,8039,7533,8772,7967,5255,8277,8447,8033,5337,5529,7521,7883,8170,5218,8255,5476,8762,7834,5712,8893,7930,7864,7628,5676,7628,8267,5730,8005,7521,5407,5448,8577,8391,5237,8613,5573,5209,7967,5612,7478,7482,8267,8052,5218,5358,5305,8783,5564,8239,7482,5622,7982,8255,7834,8015,8010,5309,7500,8919,5358,7528,5358,8554,7994,5241,7790,5261,7516,8312,7883,8249,7491,8340,7908,7972,8249,7967,8582,5401,8631,5280,5337,7706,7834,7877,5476,8093,7811,5237,8527,8365,5301,5337,5693,5688,5314,8456,7576,5476,8560,7643,8484,7834,8397,7790,7883,7500,7853,5578,5501,7653,7898,8267,7658,5606,5280,5480,7571,7653,7786,7588,7734,7616,5437,8391,7632,7653,5442,8845,8316,7972,5274,5419,7558,7482,5647,7675,5712,7692,7940,5666,7734,5534,5712,7834,5237,8929,7994,5329,5347,7834,8272,8456,8427,8104,7675,8397,8287,8140,8653,5377,5274,8762,7982,7500,5426,8772,8433,5377,5301,8312,7487,5448,5651,7768,5442,8451,8267,5218,8608,7687,8370,5629,5419,8647,8489,7628,5301,7719,8149,5476,5419,7734,5285,5237,8772,8312,8789,5476,5301,8170,5480,5476,8929,8613,8329,8811,7834,8093,5651,5314,8929,7528,5285,5237,5209,7834,8411,7774,5391,5337,5391,5391,5301,7548,7491,7779,5301,5309,7706,5688,5476,7757,8479,7999,5693,8312,5688,7849,5564,5655,8022,7904,7920,5564,8716,7757,5261,5329,5568,8732,5209,8010,7628,5396,7653,7817,7706,7628,8184,8539,8647,5237,5476,7637,5391,5647,5237,8293,7542,5606,5407,7920,7482,7482,7904,7565,8033,5426,7853,8354,7834,5564,5325,8136,7528,9542,5291,5471,8131,7482,7542,8282,5325,9550,8783,8816,5501,8267,8110,8196,7920,5407,8062,7628,7667,8805,5726,8762,8527,5671,8915,8154,7658,8756,7994,7752,8613,5661,7888,5209,8447,8461,5391,8438,7786,7864,8093,7790,5476,7786,7904,7920,7628,5426,5712,8201,5337,7828,7977,5391,7864,5213,8964,8170,8427,7706,5617,8190,7999,8222,7849,5358,8119,5573,5391,8073,7605,7710,8925,5407,8316,7542,7521,5476,7511,7542,5391,5381,8354,5464,7924,8282,8073,5381,8131,5320,5241,7715,5726,7548,9556,8484,7715,7883,7482,5476,7487,8582,9012,7823,5407,5337,5426,7805,5285,7710,7696,5717,5224,5391,8711,7977,7482,7616,7576,5248,8987,8316,5305,5666,5325,5671,7500,8504,9021,7972,7723,5629,8467,5274,8467,7768,7920,8964,7605,5730,8391,7473,5642,5274,5274,5519,5514,8391,8149,5325,7920,8613,5651,5476,7482,8687,8345,9562,5337,7719,8898,7542,8375,7588,5476,5655,7653,7872,5391,8946,5688,7675,7588,5519,8447,5377,8451,8613,7994,7533,8307,5329,5712,7843,5509,7528,8267,7930,7478,7588,7628,7877,8805,8073,7653,7977,8062,8498,8527,7877,8272,7972,5342,8190,7478,7478,8234,7482,5524,5329,5426,7542,7914,7653,7616,8104,5573,5347,8170,5274,8964,8211,7734,5448,7982,7872,7558,5524,5301,8594,7914,7605,8539,5651,5708,8397,8170,8227,8467,5407,8461,5218,8272,8227,8099,7473,8093,5325,5329,5285,8322,5237,5676,8438,5329,5352,8345,7920,5274,8387,8863,5671,9051,8571,7738,8653,8762,8827,7805,5209,5241,8543,7491,8827,8658,8772,5693,7706,7542,5314,7706,7628,5309,8721,7790,8227,5606,5237,5401,8272,7786,8451,5352,5320,5329,8249,5329,5396,8473,8144,8277,8800,8519,8239,8716,5377,8365,5419,8827,7616,8062,8329,7779,8721,8514,8509,7616,8539,5401,8039,5274,8762,5647,7994,8827,5564,8110,5296,7864,8110,8033,5377,7687,8062,7924,8676,5367,5237,8946,5237,5464,7994,5464,7999,5496,7867,8239,8062,7548,5476,8711,5509,8255,5730,7853,8329,7811,7491,8670,5647,7605,5407,8022,8005,5261,8190,7888,5381,5655,7734,7548,7994,5519,7719,5712,5213,7558,5396,5285,8149,8287,7738,7898,7946,5401,5612,5237,7511,8267,7491,8417,5564,8164,5629,5612,8322,5717,8851,7805,8073,5347,7730,5358,8940,8438,5209,7478,7616,5464,7681,7511,8509,7962,8302,5448,7800,8052,5655,5655,7632,5476,8467,5629,8077,5237,7977,7877,7538,7883,7883,5476,5342,7920,8170,7738,8196,5329,5314,7742,8443,8641,5309,5501,8190,5401,5342,5708,5573,5305,7632,5358,7994,7511,8539,8255,5401,5476,5329,5708,8946,8322,8402,7706,8249,5629,7800,5218,7877,8052,5485,8473,5320,7957,5519,8827,7610,8365,8154,5485,5347,5367,7663,8756,5314,7994,8888,7786,7542,5426,5396,5325,7734,5296,8322,8312,7516,5612,5519,8589,7558,8005,7924,5329,5386,5381,7616,5342,8010,5296,7768,7924,5629,7521,5291,5285,9021,8239,5381,8149,8217,8772,8365,5325,5274,7548,8919,8387,7616,5218,5301,5485,7675,7734,8964,7994,5661,8359,8052,7605,7610,7893,5342,5329,5676,5296,7628,5401,7616,8255,7542,5314,5666,7616,8964,7811,7491,7888,8039,8903,8170,5501,8245,7930,5476,7663,8548,5314,8527,7605,7946,8863,5325,8077,7738,8154,8745,8888,5699,7834,8381,7500,5407,5407,8375,7834,7972,5651,5301,8772,8903,8857,8582,8987,5352,7986,7605,8851,5347,5329,8473,8222,8329,5274,7834,7610,7864,5329,7528,7487,5347,8073,5407,5642,7877,7521,5407,5237,8721,8777,8077,5666,7687,5661,7710,7632,8857,8375,5372,8077,5291,5629,5320,8816,5309,5688,5476,5655,5693,7999,8312,7757,5647,7542,8732,5261,7715,8010,7757,8514,8293,8068,7637,7528,5671,8201,7658,8170,5471,8783,7548,8136,7930,8461,8282,7542,5642,8196,7667,7478,8131,8756,7565,8154,7853,8816,7904,7628,7542,5241,7542,7786,8073,7864,8312,8201,7904,8354,8857,7715,7883,7977,7828,5337,8964,8073,8093,8851,7542,7511,7768,5391,8190,5617,5358,8447,7920,7888,7478,8898,8149,7696,7994,8316,5337,7558,7977,7491,5386,5367,8903,8316,5329,7757,7482,8509,7687,7605,5274,8805,9003,8010,5396,7768,8302,7977,5642,5717,8391,5241,7719,5285,5476,7877,5248,5629,8345,7482,8613,7605,8149,7734,5261,8277,8594,5401,8946,7675,5612,5301,5241,5573,8039,5524,8964,5519,8451,5329,8527,7528,7511,8509,5407,7872,5377,7843,7542,8170,9051,8322,7888,5606,8451,7790,8543,8653,8407,7473,8653,8402,5237,7786,7632,7805,8863,5309,8827,5274,5676,8154,8057,8772,5693,5407,8170,8249,5209,7867,5291,8509,8322,8022,7864,5496,8255,7548,8676,5448,5464,7811,7663,8762,5712,7957,8005,7893,5712,5305,5655,8527,5717,8077,5426,5676,8077,8136,7511,8052,7898,5476,7977,8888,5237,8473,5629,7800,7632,7592,5218,5485,8539,8443,8190,7491,8772,5291,8239,7734,5329,8217,5347,7734,8498,8322,8919,8154,5666,7605,5314,5401,5476,5496,8473,8267,5367,7521,8227,8411,8131,5708,5459,5726,8239,7555,8227,8514,5661,8969,5261,5237,8267,7834,5358,7653,5358,7504,8509,8756,5413,8062,8489,8589,5274,5274,5407,8438,9568,0,8322,7687,5386,5358,5352,5367,5329,8411,5358,8489,7849,8721,5717,5717,8048,5291,8062,7952,8438,5476,8653,7843,5329,8805,7653,8170,8354,8354,7800,5726,7516,8227,8635,5407,7834,8438,7834,7864,7643,7628,7752,7521,7491,7542,5726,7834,7752,8282,8345,7994,7482,8144,5699,5347,8762,8239,7628,5606,8077,7738,8509,7849,5699,8519,5347,8307,5629,8005,5629,8721,8647,8795,5237,5296,7877,7920,8903,7811,7738,8179,5342,8514,8969,5261,7834,5296,5358,7752,8519,8489,7994,5274,8062,5407,8438,8756,8514,7687,5386,7952,7752,7738,8438,5329,5642,8048,5717,7849,8653,8484,8354,7843,8893,7628,7834,8227,5237,7752,7643,7834,7864,7877,5347,5726,7834,8144,5699,5717,5606,7738,5699,8795,5347,5564,5564,5333,8217,7528,7542,7478,5476,5480,7605,7924,8093,7864,7628,8282,9577,8946,8267,8821,7972,7920,5485,7565,7924,7482,5655,5381,7491,7904,5726,8539,8443,7571,7828,7696,9051,5480,5717,8312,5224,5564,5325,5329,7610,8033,5426,7888,7957,5305,5419,5320,7888,5274,7696,5459,7628,5671,5261,5337,8255,5426,8387,7558,5358,5367,5261,8119,7972,5480,8307,8467,5655,5573,5274,7790,8077,8267,5274,8456,8456,7967,5476,5407,8099,7482,8170,7805,8033,7920,8093,8272,5274,5329,8594,5274,8211,7605,7924,8705,7786,5248,5301,5301,7628,5377,7628,7610,7994,7994,8077,8217,5342,7977,5464,5381,5342,7482,5671,7834,5296,5717,8267,7632,7994,7663,5237,7994,5325,8015,8316,5708,5419,7482,5301,7648,7516,8422,8647,8222,5342,5342,7628,5274,5442,8447,8010,5325,8010,5237,7482,7864,5296,7610,5381,5301,5301,5301,8863,8756,7972,9032,7667,7823,5476,5699,7834,7542,8387,7504,8387,5325,7786,7811,7972,8131,7667,7839,5218,8170,5573,7982,8925,7632,7628,5442,7757,5342,7653,8154,8617,8721,5573,8925,5218,5325,5237,5442,7511,5301,7823,5325,8925,5261,7738,7628,7628,5529,8136,8762,5367,5391,7752,5501,8334,8267,5301,5358,8608,8093,5666,8345,8222,8249,7828,7843,5426,7478,7999,7500,5325,7715,5274,7675,7687,8805,5655,8456,5712,8093,7706,7482,8154,8302,7893,5534,8427,7482,8447,8548,5651,7516,5476,7582,7616,8387,8249,8617,8093,5367,8062,7482,7632,8349,8267,7511,8249,8888,7478,7994,8154,7482,5237,5476,7663,7883,8919,5274,5386,5329,7977,5730,5325,5337,5337,7904,7920,7994,7962,8073,5325,8349,8249,5448,7538,5301,8154,7663,5391,5491,7610,8093,7663,8519,5301,7800,5485,5419,7715,8015,7877,8745,5485,8052,8402,5485,5291,8745,5485,5485,7828,5485,7628,7893,5642,8898,5651,7746,9591,8174,7491,5606,5676,7491,5717,5693,5496,5407,8504,8539,8329,7478,8504,8196,5309,5529,8577,7904,7482,7730,8136,5305,8443,8245,8282,5476,5642,8272,5480,7977,8062,8174,7924,5209,5329,7977,7628,5325,5661,5688,8345,8727,7817,7904,8245,8255,8539,5509,8811,8484,7681,8255,8190,8571,7538,7738,8015,5224,7752,8170,7986,5661,8110,7898,5476,5688,8267,7849,5509,7715,8170,5407,8411,8539,5407,5274,8322,8131,7738,7930,8345,7533,5717,5564,8144,5237,7957,7839,8131,7977,5325,8312,5419,7990,8245,8154,5274,7542,8762,8631,5407,5261,7994,8504,5564,5712,5676,5717,7610,5309,7653,8227,5301,8110,8329,5617,7994,7834,5519,8272,8131,8267,7588,7605,7994,8800,8762,8222,8762,7994,8504,5367,7653,7994,8267,8272,8484,8484,5274,5661,5274,5325,7482,7864,8267,8670,7478,5352,7946,7616,8267,7930,7482,7482,7521,7982,7491,5693,8745,7533,7521,8598,7653,7500,7511,7500,5237,8022,8005,7706,8560,7628,5509,8282,5471,7828,8751,7533,8312,8093,7628,5485,5666,7849,8255,7864,7994,7994,5381,5314,8456,5476,7982,8370,8217,7858,5255,7706,5564,7977,7853,8827,8005,7977,7757,5606,7516,7616,7667,8994,7653,5485,8422,8048,8751,8277,8422,5381,7643,7628,5693,5712,7834,7653,5407,7990,5491,7592,8196,5476,8159,5377,7864,7864,7834,7643,8795,5218,5622,7834,5274,8676,5476,8467,8940,7982,7828,5419,7692,5666,8239,8647,5391,7800,7706,7648,8052,7478,7786,5309,7542,5296,8427,7511,5629,5301,5274,8751,5329,5347,8077,7768,7491,5693,8745,7653,7521,8598,7500,7500,5509,7786,5471,7706,8282,8022,8560,7628,5329,8093,8312,5485,5381,7977,8015,7653,7616,7616,8370,5476,8751,8919,8422,7653,8329,7616,7990,5377,8239,7864,5622,7834,7864,8077,7800,7828,8940,8052,5325,8427,8613,8751,7768,8527,8473,5372,8903,5367,8427,8473,7730,7811,7658,8577,7795,8190,7628,8170,5329,7487,8087,7924,5642,5448,8509,5514,8010,7511,5237,8239,8647,8647,5301,8473,8110,5301,8239,7723,7719,8527,5342,8527,7610,5329,8473,7730,8170,7616,5448,7521,5237,8647,8473,8473,7893,8010,8073,7893,5391,7982,7972,5325,5693,8261,5480,8631,7962,7521,8068,8504,7628,7565,7710,7482,8010,9596,9609,7482,7982,5476,5476,5358,7839,9621,7511,7839,7710,7487,8222,5391,8057,8370,5255,5606,5285,5655,5224,7839,8316,7839,7521,7924,8539,7696,7834,5622,5352,8179,8073,8028,7864,8201,7667,8307,7588,7628,5491,5642,7643,5606,5391,5407,8863,5224,5274,7877,7877,8772,7893,7588,7994,8721,7864,8699,7643,7982,5464,8964,5296,5407,5730,7849,5426,7687,7994,5564,7823,7828,5255,8073,5396,5237,8005,7511,5448,5358,7491,8267,5305,7511,7800,7715,7628,5519,5309,8845,8005,5622,5381,7710,7994,5476,5325,7648,7558,5218,8272,5296,7521,8427,5501,8543,8249,7834,7715,7877,7521,8433,8857,5358,5391,7972,5325,7706,5426,8068,5237,8504,7628,7710,8010,7482,5407,5391,8222,7487,5459,5476,7696,7648,7521,5325,5509,5285,8119,5224,8073,5666,7864,7628,8028,9633,8641,7643,5274,8721,5496,8005,5459,5448,7877,8077,5305,5320,7511,5519,5309,7800,8589,5622,7700,8087,8005,8039,7521,8484,7576,8447,5358,8073,9641,5301,7893,8033,8345,7904,5564,5274,7752,7528,8402,5358,5426,7538,7628,8527,7653,8539,5480,5480,7710,8329,7605,5661,7706,7883,8915,7730,7482,7752,7972,5325,7893,5726,8073,8222,7908,5480,5476,5726,5241,5726,7592,7768,7999,8467,5352,7576,8170,7605,8293,8170,7920,8205,7977,8093,5655,7658,5564,7675,5391,5358,5480,7920,7487,7940,7734,7858,8272,7972,8164,7478,7582,8149,7768,7914,5274,7696,8370,8073,8721,7482,7632,7538,7516,5661,5325,8170,8022,8196,8211,7779,7653,5647,8190,8422,7667,7628,7558,7528,7663,5329,8863,7576,5617,5325,8653,8272,8277,7986,7990,7706,7706,7628,7893,5514,7834,5381,5642,7478,5476,5329,7858,7542,7893,5464,5407,8154,7752,5377,8519,5367,8548,8005,8110,8087,7795,5480,8005,5448,7542,7558,7972,7511,8140,8909,5237,8340,5442,7834,8467,7967,5514,5261,5301,7972,5329,5305,5237,7632,8015,5305,5708,8179,8647,8827,5726,5358,5274,7628,7500,5396,7849,7588,7675,7516,7877,5471,8789,5480,8833,8772,5285,8964,7834,5301,8427,5485,5476,7828,7516,8888,7967,7834,7710,5329,5274,7521,5325,8617,8170,5237,7811,5301,8473,7538,7628,5480,8329,7710,5726,7828,8073,5329,8205,5391,7487,5564,8170,7967,7605,5480,8293,7977,5476,7658,7675,7538,7790,5285,5396,7632,8370,7858,7839,5301,7558,7834,5325,5285,7667,7706,7706,7990,8863,7653,5358,8179,7858,7972,5237,5514,7478,5708,7511,7542,5261,8647,7632,8827,7877,7967,5325,5285,7521,7786,5464,5301,7994,7849,8811,7920,8608,7853,5661,7667,5358,7710,5699,8539,7692,8370,8255,8131,7473,5717,8427,8845,7858,5358,7990,9012,7930,5342,7957,8073,7675,7616,7849,7839,8739,8888,5305,8608,7957,5386,8762,8205,5342,5377,8888,8739,8479,5524,5564,5688,8119,8255,5329,5372,8473,5301,8653,5377,7565,7533,8721,7790,7883,5320,7576,8048,7681,5352,5209,8048,8297,8811,8174,7920,7790,5564,7706,8164,5726,7521,5519,7795,5325,5352,9361,8670,8438,8391,5301,5391,8548,5377,5352,8022,7622,7768,5712,8598,5407,7888,7982,7986,8484,7706,5401,5655,8888,8245,5305,8589,7800,7800,9026,7500,5712,5305,7605,8789,7811,7908,8539,8888,8577,8261,8307,5209,5386,8062,5237,8307,7653,7610,5342,8484,8227,5459,7628,5301,5676,8190,7542,8582,8548,8711,5655,8987,7628,7952,5305,5325,7706,7999,8005,7643,7904,8312,5712,7500,7834,7675,7779,7628,8422,7706,7599,7779,7994,8277,7710,8647,5606,5305,5325,7500,5391,7834,8427,7628,8479,5241,5301,8277,7817,5480,7757,5342,8762,7864,5358,8302,8560,8068,5329,5476,5381,8010,7576,7628,5329,7752,7982,5426,5237,7565,5501,8136,5337,5712,8762,7849,5367,5726,7653,7920,7872,5480,7500,5688,5255,7622,5358,5325,7994,8170,5568,8903,8033,7990,5448,8184,7723,7658,8222,5564,7687,8115,7786,5476,7757,7742,5305,5381,5320,7957,7710,7696,8345,7994,7681,7605,7482,8577,7632,8316,8255,7706,7605,7994,5285,8598,7952,5274,5717,8795,7491,7500,8316,8711,7888,7667,5274,7800,7675,5296,8427,5407,7628,7999,5578,8234,8845,5514,7478,5688,5642,5564,5509,5337,5301,8302,5301,8582,5291,8190,7746,8154,5329,5218,8447,5209,7706,8745,7521,5629,8104,5325,7757,7867,7516,7576,8851,8277,7893,8447,5476,8653,5329,8484,8227,5485,8170,8093,7967,5301,8676,7663,5285,8184,8154,8126,5391,5693,7914,5325,5391,5730,7478,5480,7692,8322,5325,8010,8582,8903,7538,8302,7883,7994,7628,8658,8110,5459,5464,5401,7834,5688,5688,5407,5501,7864,8387,5564,8159,5407,5367,5622,7982,5471,7533,7834,7752,5407,8052,5305,7687,5726,7511,7632,5391,5391,7482,8533,5655,7738,5642,7715,8164,7542,7706,7752,8893,5352,5401,5708,5333,5296,8073,5325,7681,8925,5407,5514,7800,5237,5688,8473,5325,5325,7605,7768,7605,5622,8154,8015,8756,5529,5432,5291,5726,7521,8073,5285,7834,7588,5352,9021,5261,5261,8010,5291,7516,8329,7622,5237,0,8946,7675,8721,5730,7908,8062,5514,8427,5367,8211,8119,5301,8227,7935,7864,7511,7616,5325,7500,7834,8302,5301,7628,5301,7763,5655,7521,7864,5329,8721,5301,8946,7628,7834,8863,5337,8554,5329,7565,8745,7516,8073,5255,8184,5329,7757,5407,5564,5329,8222,5730,5699,7658,7952,5381,8217,7994,7696,8099,5509,7828,8461,5325,5285,5274,8821,7521,8329,8845,5301,5401,5407,5301,5391,7681,8234,8302,5509,5655,5325,5367,8745,8093,5386,7967,7893,8653,8227,7786,8447,8184,7757,5485,8190,5391,5209,5501,5459,8184,7588,7864,7839,7511,8159,7864,5688,5564,5464,7994,8940,8411,5514,5708,7706,7738,7482,5642,7994,8267,5333,8473,5529,5432,7800,8438,5218,5622,7877,5291,5261,7516,9021,5329,8795,5301,8033,7482,5453,5476,5564,5305,8484,8201,7834,5564,7757,5325,5325,5261,8811,5391,8527,8391,8222,8467,7972,5693,8196,5564,5501,7752,5407,8539,7628,8998,5285,5291,5291,7920,7811,5655,8222,7834,8387,8154,8533,5666,7990,7742,5325,7977,8903,8093,5419,5564,5480,7628,5301,8411,7972,8509,8467,7628,5726,7675,7972,7883,8170,5337,7786,8936,7628,7924,5666,8387,8964,7628,5301,7616,8863,7478,7696,7795,7663,7610,7834,8073,8617,5325,8577,7605,7834,5391,8062,5301,8613,8711,5717,8504,5285,5519,7576,7972,7632,5274,7972,7706,7972,7977,5325,7628,7811,5476,8201,7478,5676,7628,8073,8329,5651,5358,5237,7667,8170,5647,5485,5377,7864,7864,8582,7982,7478,5651,5329,7628,7521,7478,5255,5320,5564,8345,7999,5352,5325,5524,7930,7757,5329,5699,5407,8170,5647,8539,8467,7706,5301,7491,8863,8721,8863,8093,8119,7706,5485,5476,7972,5485,5209,8277,7582,7706,8104,8845,8170,5726,8154,5655,5325,5320,7478,7710,7924,8110,5476,5688,5377,7982,5377,7864,5381,7790,5564,8104,5647,7834,7849,8467,7858,8267,7706,7982,7616,5471,7715,8170,8772,8777,7565,5301,7616,8447,8190,5534,5391,7849,5573,5629,5367,8893,7511,5325,7786,5407,8196,7653,5209,5519,8447,5642,7834,5325,5301,5606,5568,5377,5224,8447,5647,7538,5285,5524,5606,8033,5301,5622,5578,8845,5642,5476,7742,7710,7972,8239,8164,7957,5358,8617,5314,7920,7478,5291,7482,7768,7487,5285,7924,5396,5358,7972,7588,5464,8010,5285,8239,7834,8316,8484,5274,5274,5647,8349,9021,7628,7616,5329,8641,7605,5529,5476,7920,5320,7632,7706,5476,5325,5578,7628,8427,8811,7924,5377,8381,5647,5651,8839,8249,8539,8354,7628,5666,8222,5329,7898,7610,5325,8354,7616,8721,8873,5301,8375,5453,8484,5325,8467,8391,9471,7742,8387,5237,8222,5655,7786,7582,5329,8387,7548,7924,7500,7972,5726,8467,5401,5480,7487,7972,7696,7795,5519,8479,7977,5717,8375,8062,7786,8073,5301,5676,7982,7864,7628,5274,5377,7478,5699,7622,8345,5325,7930,5352,5688,8104,8845,5476,5464,7982,5325,7864,7786,7734,5209,5712,7715,8159,5642,5342,8789,7719,5296,5407,7849,7628,7616,5578,8239,8239,5285,8484,7628,5329,8473,8427,7616,8154,5301,8721,5529,5329,8484,7723,5407,5274,8451,7628,5407,7834,8451,5329,5337,8140,5325,8473,8533,5642,7605,7667,7605,5407,5651,5651,7478,7511,8033,7478,7952,8447,7706,5329,8484,7667,5285,5301,7592,7834,7977,5651,5301,5617,7883,7516,5485,5325,7500,7588,8140,8349,8349,5717,7482,5564,7746,8387,7628,8174,5726,5642,8711,7681,8783,7628,5726,7746,8170,8527,5564,5726,7746,7538,5305,5305,5296,7610,7967,8402,5501,5464,5464,8255,7723,8307,7853,7946,7867,5301,7946,5320,5301,8184,7710,9372,9372,5476,7542,7883,5237,7622,5305,5305,7542,5491,5573,5337,7828,5476,5367,5476,5726,7667,7834,5485,7628,5301,8903,8762,8179,7482,5726,7834,8772,8345,5655,5622,8179,5476,5712,7883,7834,8762,5485,7972,5501,5564,5564,8438,8438,8467,8140,5726,9646,8762,5325,7675,8211,5708,5432,5480,5358,8467,8387,5480,5693,5651,8716,7675,5209,7786,5358,7582,5501,8272,8467,7920,8227,5524,7706,8136,5629,7786,5320,5320,5501,7893,7752,7904,5564,8604,8316,5726,8119,5391,5337,5391,8805,7478,8184,5485,7643,8170,7834,5647,5377,5237,5274,7834,5407,5358,7610,5688,7653,7632,7478,5730,5237,5717,7687,5274,5534,7687,8789,7700,7706,8533,5325,5209,5325,5325,7706,5325,7710,5325,8033,7849,8387,8800,5661,8174,5358,5391,5209,7710,8184,8170,7681,8093,8093,5573,7768,5661,8800,8077,7653,8316,8287,8653,7628,7940,5476,8447,8451,7967,5726,8805,5274,7742,7864,8494,5476,8287,7967,7828,8267,9654,8387,7710,8033,7734,8277,8170,8093,8174,7681,5209,8800,7628,8287,5726,8494,7610,7710,5241,8334,7834,7610,5329,7817,8795,7516,7940,7738,7610,7817,8795,7952,5688,7768,7952,8354,8721,7849,8604,5661,8073,8115,7867,5480,7632,7883,7675,8893,5407,8190,8411,7962,7521,5274,5255,5655,8119,7982,5391,7605,8354,8104,5329,7478,7582,7675,5325,5381,5464,5426,5712,8010,7805,5622,7858,8527,7605,8062,7867,5730,5688,7622,7542,0,8005,9657,7957,8267,7849,5629,8354,5442,5524,7893,8893,5241,8131,8077,7542,7675,8498,7817,8334,8762,5301,5358,5314,5573,8635,7710,7605,8322,7610,5347,5407,8201,5301,5296,5358,8539,8827,7834,5352,9367,5622,5213,8647,8307,7628,7763,5622,8115,5237,8039,5476,7982,7877,5568,8140,5255,5442,5325,7952,7588,7957,7687,5329,8184,5612,8498,9662,5612,5333,8676,9670,7675,8073,5651,7487,8903,8239,7883,7828,5564,5407,8438,8005,5491,8255,8919,5464,7962,8149,8365,7675,8762,5391,8365,9676,5213,9680,5213,8811,8170,8762,5329,7834,8073,8903,8159,7990,7610,7542,5401,8073,7576,8277,9686,5347,8005,7628,8354,8052,5655,7658,5377,5651,5442,5325,9690,9680,8909,5622,7576,8010,5448,7576,9695,5314,8909,5329,5329,5407,5480,7616,7628,8277,7610,9701,5401,5347,7542,8354,7548,5726,5401,8411,8676,8857,5301,7487,7632,7616,8354,5314,7500,5651,8539,8010,7914,5578,7914,8293,5661,5666,5274,5209,8447,8879,9710,7542,9715,5358,8282,7478,8302,8756,8110,5501,5358,8402,9721,5301,7710,9726,9736,7904,5301,8811,8170,8312,5305,8140,8184,7576,5301,7588,7681,5301,7706,7588,8062,8316,5209,8604,8354,7576,7516,9345,8126,9740,5501,5325,9745,9751,7779,5274,5564,7779,8179,8845,5301,8354,5329,8170,7706,8402,8126,8015,7516,8548,5485,8039,9759,8589,8589,5629,8411,8110,7746,5730,7500,8548,7982,5407,8365,7839,5301,5661,5564,8447,7930,7786,5519,5524,5629,5688,9751,8340,9345,5629,5651,8589,5501,5372,8756,5419,5305,8589,8589,7495,5237,7779,5419,7908,8073,7811,985,8548,5274,7495,8641,8077,5301,7571,8539,8077,7681,5347,5651,5285,8484,5237,7817,5241,8484,5337,5325,5496,7576,8329,8354,8062,7628,7849,5509,8504,7752,7752,5688,8136,5209,7853,8816,8998,5381,5320,8104,7920,7482,8302,8062,7972,5325,5501,8267,8504,7883,5309,7588,7565,5612,8402,8484,7687,9766,5309,5564,7972,7839,5209,5442,8062,7687,5666,5237,5564,5661,7914,7930,7605,5726,8093,7605,8670,8811,7924,5564,5480,5655,5491,5314,5391,7548,5274,8443,7592,8282,8170,5305,5320,8397,9772,5485,8504,7946,5661,5381,7605,5442,7795,8126,5291,8582,7982,7839,8845,8845,8272,5661,7692,5285,7768,5671,7632,8190,5224,5274,5564,9462,8316,8164,7681,7482,5666,5661,7768,7500,8099,5459,8316,7487,5419,7521,5296,5291,7972,5407,5676,7710,7746,5655,5329,7558,5688,5320,7898,8316,7675,5509,8196,8845,5519,5377,5519,5661,8062,5237,5564,8805,5342,8217,8653,5564,5485,7982,7972,7834,7592,8527,5325,9778,9783,8548,7516,7888,5301,5329,7779,5442,7768,8670,7706,7706,9032,7706,5509,7542,5274,8087,7994,7706,8272,5509,8461,7599,5413,8863,7774,8039,8402,8099,7511,7706,8427,7930,8287,8093,7746,7706,7867,8772,8548,8234,7967,5476,5209,5426,7779,5309,7893,7872,8438,7628,8548,8010,5329,7500,9789,8015,9796,7500,7521,5407,7990,5274,8821,7746,7706,8676,7548,8539,8438,7622,8467,5661,8222,7487,5688,8411,7811,8845,8827,7746,7994,5564,7994,7628,8131,7622,5661,5401,8548,8411,8267,7482,5377,7795,7972,8267,7558,7877,5491,5367,8467,8548,7500,9804,9810,8845,9367,7616,5309,5476,8863,8447,7898,7786,7533,7576,5407,8451,7768,8617,8267,5367,8952,5237,8164,8272,5564,8267,5655,5524,8613,7982,5437,8909,5274,7588,5655,8548,8272,9818,7774,5358,8527,8140,5301,5612,7482,5237,8548,5666,5622,7542,7800,7972,5329,5305,8647,5401,5651,8190,8548,5325,5261,5342,8010,8952,5413,5476,5514,8711,7576,8322,7643,9824,5237,9829,5519,8297,8190,5296,8255,8833,7571,7972,5261,8589,7516,7628,8010,8239,8287,9021,5407,7648,7877,7872,5519,5296,5396,8857,5237,8322,9835,8245,8119,5296,5237,7719,5407,5419,7811,5372,8827,5381,5285,8073,7877,5476,5386,5342,5476,8635,7500,5325,8888,7542,5347,7734,8329,8857,7706,5352,5301,5305,5666,5661,8857,8582,5337,7500,5666,5274,7877,5568,5329,7877,7616,7774,7521,5274,8397,8857,7632,5237,8479,5347,5666,5401,7487,8484,7628,8062,7576,5274,7849,8354,5325,5509,8582,5325,8504,7994,8267,8062,8104,7920,8302,8461,8136,8504,8548,8099,5501,7972,7752,7565,7605,7482,8816,5320,5274,8811,5661,5564,5401,8443,7914,5237,7972,7548,7576,8282,5564,5314,8670,5476,5485,5491,7898,8164,5381,7692,7648,5459,7946,5296,9462,8190,5419,7632,5291,5285,8316,8845,7675,5676,5519,5237,5342,8272,5325,5377,5325,7500,8093,9839,7500,7706,7893,8287,8015,8234,8119,7994,7746,8087,5413,5442,8272,7511,7768,8548,8427,5509,5329,8863,5209,7990,7521,5407,7746,5347,5377,7622,7706,8267,5296,7487,7888,8676,8411,5274,8131,8863,7811,8539,5329,8411,8438,8467,5309,5564,8613,5655,8527,8952,5524,7877,5301,5476,5407,8888,8647,7800,5666,7616,5612,8711,5514,5661,8239,5261,8833,5419,7542,5372,7811,5285,8857,8998,5661,8461,8998,5699,8998,9844,8073,5255,5730,9853,8461,7616,7616,8745,5367,8721,9021,8772,8227,7706,5534,7616,5666,8772,5329,8721,7599,7599,8461,7599,7653,9858,7632,5564,5651,8048,7786,7605,8170,7790,7786,8438,8461,7904,7795,5377,5676,5564,7516,7834,7999,8397,7795,5314,7628,7972,8345,8048,7823,5320,7487,7920,7538,7511,7904,8048,5301,8783,7994,5358,5325,8833,8316,7511,5325,5676,5651,7999,7632,7920,5325,5325,8375,7924,7763,8322,7738,5712,7924,5337,5337,5501,7904,5524,7924,7786,8322,8205,5524,8093,5476,7605,8539,5519,7542,5476,5274,8164,8658,8658,7542,7811,8397,5274,5476,9863,8322,5274,7628,8845,5381,5629,8467,5237,8010,5261,5501,5261,5401,5666,8845,8417,8322,7877,7628,7817,5476,5248,5534,5501,8062,7710,7710,8402,5564,5367,9054,5476,7986,5476,7920,8093,7511,7972,5726,5237,5661,7521,5237,5237,7706,8255,5296,5296,7675,7719,8062,5419,8539,8402,7893,8272,8302,7710,7605,5407,8190,5391,7858,8548,5509,5296,5730,7849,7849,7653,5708,8647,5476,5401,5726,9873,7516,9054,8255,5708,8010,5476,5296,8190,8479,5642,8149,9877,8898,7790,5578,7706,8131,8131,8131,5459,5459,7696,9045,8821,7696,8716,5622,5666,8805,5209,8845,8783,8052,8267,8994,7920,7710,7487,5573,7977,8170,7849,5573,8149,8099,5325,5629,7742,7957,8417,8131,8334,8115,7957,5325,8149,8154,5337,5305,8359,7696,5578,8805,7491,8245,8022,8196,8267,7710,7883,7883,8062,8845,7972,9881,8077,7883,5655,7935,8093,5241,5337,8631,8239,7610,7972,8381,7681,7696,8387,5459,7795,9886,5401,8052,8119,8577,8751,5485,8170,5358,8539,8039,7752,7500,5255,8827,9891,9900,8805,7628,8827,8005,9904,8005,8827,5401,5534,8005,5401,5401,8827,5708,8119,8022,8805,8005,8077,7972,7883,8381,7867,5325,8805,8805,7610,7628,7628,7999,7487,7588,5476,7588,7487,7999,7999,8110,5476,5333,5325,5688,8800,8340,5717,8539,7811,5464,5476,8504,7675,8613,7576,8467,8762,8461,8164,8811,5496,8494,8227,7786,7972,7653,5564,7893,5501,8456,8334,5407,7565,9032,5337,8136,8560,8800,8297,7817,7628,7752,5642,5309,7920,7542,5237,5237,7893,8800,5485,7658,7914,7548,8015,5661,7914,7994,7576,5459,7883,7893,7904,8073,5726,5606,7930,7795,7658,5209,7692,7972,8447,5255,5209,5564,8334,5655,5274,7571,8456,7977,8119,7972,8222,7715,5726,7982,7990,5476,7482,7500,7924,7610,7582,9012,5329,5485,8293,7834,8334,7473,5476,8687,5671,5717,7582,7972,7675,7504,7504,7696,7864,5426,7858,8766,5209,7952,773,9915,2533,3110,9919,3464,9924,2802,9929,9934,9939,9945,3447,2826,2847,3306,9950,9955,2550,2661,3190,9961,2685,2300,3133,3157,2871,3215,9966,3179,3005,9971,3235,2606,2953,2977,40,609,2526,9977,1783,9981,3457,9985,2795,9989,9993,9997,10002,1733,2819,324,3297,10006,10010,2547,2654,3120,10015,2678,2700,3126,3148,2864,3208,10019,347,2998,10023,3228,2599,2950,2968,24,603,10028,429,10033,852,10037,803,972,800,10041,9312,10045,10050,829,833,985,806,955,959,10055,839,819,816,10060,826,988,1044,1464,926,809,10064,855,836,1659,10068,915,845,842,848,776,10073,4081,3560,10077,3573,10082,10087,10091,10096,10101,10107,4021,10112,3609,4035,10116,10121,3809,4003,10127,10131,3849,3912,10136,10140,3707,10145,10149,3959,3661,10154,10160,3747,3787,10164,64,606,2523,10169,3103,10173,3454,10177,2792,10181,10185,10189,10194,2738,2816,2840,3293,10198,10202,2544,2651,3183,10207,2675,2696,3123,3144,2861,3205,10211,670,2995,10215,3225,2596,2947,2964,52,1084,10220,2537,9220,3114,10225,3471,10229,2809,10233,10237,10241,10246,10250,2759,2833,2854,1321,10255,10259,1426,2668,3194,10264,1325,327,3137,3166,2878,3222,10268,1318,1311,10272,3242,2613,1400,2986,32,410,10277,1620,10282,482,10286,3468,10290,2806,10294,10298,10302,10307,10312,10316,3451,2830,2851,3311,10321,10325,2554,2665,432,10330,2689,2305,464,3162,2875,3219,452,1772,3009,10334,10339,3239,10345,2610,2957,2982,106,10349,0,0,0,10360,10360,10360,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,8,10,12,14,16,18,20,22,10360,10360,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10366,10366,5777,5777,4344,4344,5078,5078,6688,6688,6694,6694,10373,10373,10379,10379,5898,5898,6704,6704,670,670,6704,6704,10387,10387,10394,10394,6704,6704,1736,1736,0,0,10402,10402,10402,10402,10402,10402,10407,10407,10417,10417,10427,10427,10434,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10446,10446,10450,10450,10455,10455,6540,6540,10460,10460,2851,2851,3613,3613,10465,10465,10470,10470,10475,10475,4198,4198,3917,3917,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10480,10480,10480,10480,10480,10480,10480,10480,168,179,184,10488,193,168,179,184,10488,193,168,179,184,10495,193,618,618,618,10505,249,249,249,249,10511,10520,10520,10527,10527,10527,10527,10541,10541,10546,10546,10549,10549,10558,10558,10558,10558,4971,4971,770,770,1451,1451,908,908,5150,5150,5150,5150,1521,1521,28,28,44,44,44,44,44,44,10568,10568,46,46,52,52,52,52,776,776,54,54,54,54,54,54,56,56,56,56,58,58,10575,10575,66,66,10579,10579,10582,10582,10593,10593,10593,10593,10599,10599,5234,5234,4924,4924,10604,10604,10608,10611,10615,10619,10623,10575,4971,10627,10631,5070,5070,5070,5070,5070,5070,5070,46,46,5070,5070,5070,5070,5070,5070,10634,10645,10651,10658,10658,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,34,54,48,40,48,24,40,10667,64,32,52,10676,1311,1314,3242,4177,875,2878,3342,3222,10684,3648,4056,2514,10688,2854,1321,3194,10255,1400,3471,3373,2809,10229,2613,2668,1426,10693,1325,2537,24,40,64,32,776,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,836,911,915,922,926,812,809,848,806,955,816,842,803,968,800,845,1144,1454,1148,852,1464,1044,24,855,819,839,988,826,429,24,40,64,32,52,479,2885,833,1060,773,852,855,933,937,942,951,10697,10707,5004,10717,819,819,863,0,0,0,0,0,0,0,0,0,0,0,0,875,884,24,770,40,892,64,895,58,664,46,1057,32,773,905,52,776,908,836,911,915,918,922,926,812,809,929,848,933,937,942,946,951,806,955,816,959,842,803,968,800,972,845,855,819,839,985,988,992,826,429,976,10727,770,40,892,64,895,58,664,46,1057,32,773,905,52,776,908,1011,0,0,0,0,0,0,0,0,0,0,0,0,6,8,10,12,14,16,18,20,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,8,10,12,14,16,18,20,22,836,911,915,922,826,988,1044,848,806,10733,842,803,968,845,816,800,819,855,839,852,955,429,985,926,24,109,40,776,0,0,0,0,0,0,0,0,3715,10737,836,915,922,806,816,842,803,800,845,926,809,848,826,819,839,855,852,429,10041,10068,10060,10742,24,40,64,32,905,52,908,1786,10747,106,50,58,38,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,40,64,32,905,52,836,911,915,918,10750,922,812,10755,809,929,10760,10765,10769,806,955,816,959,10774,842,942,803,10778,968,800,972,10782,845,1073,855,819,839,985,992,826,429,770,40,892,1518,64,109,52,905,908,1440,855,819,839,852,0,0,0,0,0,0,0,0,0,4580,4580,4580,4580,4580,4580,4580,4580,4580,4580,4580,4580,4580,4580,0,0,0,6,8,10,12,14,16,18,20,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7558,7681,7972,8402,5712,5661,8402,5661,5305,8349,5325,10786,0,7908,7730,8205,7706,5386,5651,8789,8375,5509,8073,7752,8721,5688,5285,5485,8110,7946,8451,8577,5261,8805,5712,8104,7914,5661,8407,8397,8140,5519,7528,5480,8174,7558,8028,5564,8307,5480,5453,10798,7920,7786,7795,5301,5401,8015,8402,5381,7779,7576,7920,8473,8099,7858,8039,8005,7805,7883,5606,8028,5485,5381,7986,5476,8345,8504,7914,8287,5564,5564,8170,7893,8322,5509,7582,7920,8039,8548,5693,7478,5712,8821,5622,7972,8694,7795,7742,8539,8345,5426,5386,7628,8732,7823,8267,7548,8110,7864,8159,5647,5642,5407,7994,5285,7628,7977,8676,5358,8227,5564,5274,8447,7746,8631,8909,8422,7898,7542,5329,8631,5448,5629,7588,8863,5717,5476,8073,8239,5325,7920,5661,5524,5329,8888,7742,8227,5329,8039,8334,8663,8647,5419,5564,5629,5305,5237,5622,5329,5285,7616,5329,7972,10803,8104,5401,8316,5352,8447,7687,8239,8402,7952,7675,8909,8010,7972,5285,5261,7478,8402,8489,7521,5342,5501,7616,8772,9361,7883,7908,8789,8068,5401,5358,5218,5291,5573,5476,5676,7986,5401,5514,8068,7786,7706,8402,8888,8329,8613,8154,8205,5274,8473,5426,8739,7986,7834,5329,8154,8205,7632,5726,8205,8375,8447,8205,8033,7542,8494,8687,8205,7516,8687,5209,5285,8077,5442,5325,5524,5442,7746,8077,7972,7972,5717,5407,5459,7972,5519,7658,5655,8484,5274,7628,5496,5642,7628,5248,5237,5237,8484,7628,7628,5426,8170,8484,7528,5617,5617,10809,8484,5391,8772,5320,5391,5606,7883,7734,7734,8721,5396,8772,8732,8598,5367,7478,7576,8479,8461,5261,5476,0,8994,7658,8010,8282,8732,8560,7565,8282,5699,10817,7548,5407,8777,8093,7605,8115,7888,8073,5274,5717,8732,7977,5329,8062,8334,5237,0,5564,5372,8527,5301,8670,7628,7843,8805,0,7994,8073,8249,8184,8170,5464,8087,8548,8411,7790,5459,10825,7898,5496,5476,5514,8777,8340,7957,5491,10830,5642,8316,10836,8903,8772,5329,5476,5381,5407,8154,8249,7706,5329,5329,7877,8617,8682,7779,5407,5407,7719,7834,5564,5255,7834,7834,8340,8340,5476,5333,8805,5688,7663,8716,7681,5285,5381,5352,5509,5476,7637,7746,7763,8811,7542,7628,7628,8851,7723,5647,5367,7706,8211,5209,5296,5342,7972,7521,5358,5564,8068,7994,7478,7972,7706,5261,7752,7542,8504,8998,5568,5381,5329,7500,5325,8196,7706,8467,5501,7904,7872,7528,5325,5396,5396,7565,7478,5291,8131,7972,8267,8196,5708,5573,7790,8387,7893,8705,8560,8827,7482,5337,8411,5476,10844,7994,5301,7920,7864,5274,8227,8777,8249,7588,5476,8467,8297,8093,7658,7681,8539,7622,7723,5301,5218,7990,7715,8456,8222,8131,8438,5476,5476,8170,7920,8893,7692,7605,8447,8073,5224,5261,5688,5642,5274,8543,8062,7616,7687,7582,8170,5459,8077,8811,7982,5255,5712,5564,8119,7786,5358,7994,5564,5314,7790,8010,7867,7790,7790,7904,5209,5305,5209,8539,7710,5325,5485,7610,0,5453,0,5529,7616,7723,5717,8391,5301,8164,7482,8682,7542,7637,8033,7628,5519,5209,8577,7478,5325,8005,8604,5248,7839,8131,8687,8211,7710,5285,5285,5564,7628,7605,5209,5419,5296,7482,5496,8048,8164,8613,5248,8211,8316,7548,0,7605,8329,7811,7473,7768,5671,8245,5485,5426,7768,8340,5485,7696,8345,8087,7710,8504,5407,5377,5464,5255,5301,5309,8149,5320,9345,8245,7977,7648,5358,8115,8255,5296,7706,8903,0,8164,7643,7616,7632,7616,8329,5309,7962,7482,8349,8062,7576,5642,8762,7994,5573,8307,5564,8504,7482,7849,5358,8201,8267,8539,5647,5342,5342,7558,8033,7478,5432,5237,8845,5301,5564,7605,5305,5301,9345,5325,7888,5688,8302,5612,8033,8527,8170,5712,8249,5426,5485,7935,7528,7667,7834,8805,7757,8451,5509,7653,5337,8451,7977,7599,5337,8816,8670,5274,7843,5564,7864,5305,7752,5381,7823,7715,8795,8277,5401,5391,8494,10850,5401,8154,5337,7616,7616,10857,8104,8211,8211,7839,5509,5485,8539,5726,7582,5407,7967,7786,7986,5329,8170,7994,7667,8077,8653,7849,7734,5209,5524,8039,5564,8099,8227,5442,8022,7805,8427,8514,7774,7904,5301,5647,8073,7653,8222,8196,5676,5237,8196,8539,5381,7734,8903,5309,7999,7658,8845,5396,7786,8093,7893,5309,8345,7599,5413,7576,7834,8170,5407,7516,8772,5320,5606,8479,7982,7706,7849,8402,8272,8104,7930,8227,7811,7893,8277,7706,8249,5255,7994,7883,8302,7811,7834,5476,7681,7706,5509,8604,8716,10862,5480,8484,8964,7632,0,0,8249,0,10874,0,7616,7616,7616,7675,7730,5688,7990,7521,5509,7628,7628,5564,5401,8190,8467,7823,8115,8494,8104,7675,7628,7858,7790,8345,7864,7632,7616,8154,7795,5419,7746,7571,7571,8170,8222,8110,5237,5661,8345,5726,5407,5564,5329,5485,9054,8527,8062,8062,5730,7834,5647,7605,5209,7521,7914,5717,5381,5485,5651,5464,7914,7734,5671,7582,7576,7542,7994,8073,7994,8994,5325,7982,8205,7977,8676,5642,7667,8827,7768,7994,5274,8005,8805,7643,5237,5391,7706,5347,8548,5642,8467,5712,8267,5320,8539,5622,8721,9751,5529,7877,8509,8005,0,5419,7957,7994,5213,0,5448,7482,5391,8104,5391,8267,5655,7843,8293,8249,7849,5699,7940,8467,5301,8170,5237,8334,5573,7500,5647,7511,8494,5655,7542,8443,8312,8451,8467,5573,7628,7908,7533,7734,7734,8136,8851,5237,5501,8267,5241,5407,5325,8893,8777,8869,5651,8052,7962,5655,8391,5448,7500,7511,5237,7786,7843,8164,5534,5671,8451,5391,5629,7632,8613,5612,5274,7930,5642,7952,5358,8548,8234,8925,5519,8604,8370,5347,0,5325,5301,8179,7843,7628,0,7616,5305,7757,7681,7790,8316,7605,5352,8345,5401,8827,5237,5255,5688,5309,8104,5381,7994,8422,8340,7628,5476,5325,8239,5358,5329,7588,8201,8015,7632,5419,7800,7834,8093,5325,8647,8234,7653,8539,7588,5329,5617,8987,8539,7763,8329,7800,8073,8539,8451,5578,9367,8190,7994,7982,5708,5651,7957,8140,7548,7774,8312,5419,5651,8827,5453,5296,7667,7482,5407,7834,5320,7482,8005,8033,7982,8302,5568,7542,7538,8205,10880,8322,8307,5401,7516,5568,7663,5358,5301,7994,5325,8329,8119,8010,8249,7742,7877,7828,8329,5285,7872,7872,5666,7628,8255,5573,5396,5407,8349,8267,8222,5337,5726,5329,7648,5296,7864,8909,7893,5291,8397,8048,8613,5501,8005,8598,7872,8387,7500,5564,5291,8903,7478,8316,7487,7675,5237,7610,8267,0,7706,5407,8267,7962,8093,7628,5367,5218,5325,8062,7487,8789,5666,8190,8267,7628,5237,8052,8073,8052,7663,7994,5296,7994,5325,7839,7521,8375,7763,5476,8873,5419,8179,7914,8322,5401,7990,8467,8245,5377,5629,5661,5358,8427,7962,5401,8010,5693,5381,7786,5661,5320,5407,8898,8062,7730,7528,8329,5642,10888,5448,5320,8329,8217,8751,7994,8766,5442,8349,8613,8647,7658,7706,5372,8033,5655,7696,8447,7616,5524,8827,7675,7706,8048,5407,7500,8005,5381,8411,7757,5651,5347,5309,8777,7622,7723,7867,5274,10893,7742,5274,7588,5329,5476,7500,5661,5301,5476,7986,5666,7675,8539,7972,7930,8909,5642,5305,7811,8647,8010,5448,8772,8422,7723,8255,7994,7511,5296,0,5524,8267,0,5301,5320,5573,7663,7516,5372,8190,8033,8277,8154,5480,7478,7872,7872,7706,7946,5329,7588,8422,8211,8267,8751,7628,8329,5325,7592,5274,5305,10898,7588,7592,0,8077,8073,8261,5476,5274,7628,8170,5401,5401,7482,8322,7616,7610,8316,7994,8354,8093,5726,5642,8010,5651,5347,8005,5347,5325,8903,9516,5666,5666,5396,8527,7972,7994,8005,8427,8359,5301,5325,5651,5666,8359,7632,5325,8005,5329,5407,8589,0,5666,7768,7675,5476,8375,8514,5432,7982,7982,5329,9641,5329,7487,7675,7542,5573,7982,7500,8451,5564,7500,7500,5329,7982,7628,8052,5285,8170,8039,8222,7834,8427,8427,8005,8282,5237,5320,5248,5688,7757,5325,5688,7558,5391,0,8539,5578,5617,7628,8811,5209,8062,5296,8249,5717,5342,5367,7637,5391,5476,7538,7521,7565,7864,8994,7710,7542,7752,7752,7872,7898,5358,8354,8354,7667,7706,8297,7482,7706,5606,8467,5291,5296,8196,7706,7548,8387,7839,7715,7599,5501,5501,8560,7817,5372,5564,8297,5381,5564,7710,8307,8647,5274,8447,8594,5688,5337,7687,5255,7710,5337,7849,7653,8170,5301,5564,8297,7687,8131,7920,8073,5274,7605,7757,5432,5224,7786,8297,5726,5617,8964,8093,7786,7883,7786,7616,5301,7558,5726,8711,5717,7839,5296,5296,8211,5564,7924,8631,8577,7605,5476,5274,8144,5285,5301,5476,5255,5325,5201,5617,5476,7599,5305,7723,7867,7952,7817,7972,7800,10904,7972,7977,7648,7487,7706,5426,8577,5688,8670,5218,7977,8839,7935,8126,7864,7653,5320,8190,8527,7914,5655,5564,5274,5578,7478,5564,5301,7843,5358,5573,8467,5337,5381,7883,7834,8805,7828,8839,5296,7795,7972,8022,7893,8170,5651,7706,7706,7628,7667,7687,8249,8322,7487,5237,8099,5301,5459,7681,8272,8653,8811,7653,7628,7898,7893,7920,7994,5471,5476,8467,7588,7768,7542,7872,8447,8745,7994,7779,8039,5509,7582,7864,8282,8539,7599,8179,7924,7616,8039,7877,0,7986,7990,5325,7849,8805,8349,7500,7746,7628,5381,5391,8190,5464,7834,8411,8110,7994,5564,8467,5712,7994,5564,8821,5661,5337,5688,7790,5367,7616,5391,5248,5629,7605,8548,5377,5564,8467,5296,8249,7986,8762,7982,5688,5459,5301,7795,8267,8170,7823,7957,8658,8164,7616,5419,0,7843,8334,7487,5237,7839,5237,8467,5534,5401,5448,8282,8164,8164,5325,7478,5688,7653,8196,5476,5358,5396,5629,5476,8762,8716,7849,7982,5296,7538,8391,5476,7752,7616,8909,7628,8777,9423,10904,7800,0,8987,5578,8340,7710,8539,5708,5329,7994,7972,5629,8033,8647,5726,7920,7628,5407,7632,7957,8196,7548,7482,5419,8952,7752,5285,5509,7972,5578,7538,7538,8039,7511,7790,5237,8827,5301,7800,7675,10909,8015,8211,7521,5305,8397,7487,5688,8594,7972,5352,5352,5285,5726,5432,8239,7533,7952,5237,5325,5622,5464,7893,8149,5291,5291,5296,7482,7877,5285,5261,7648,7675,8711,5248,8329,7487,10919,7558,8903,5301,5661,5261,5476,5485,8245,7877,7706,7839,7521,5617,7632,8196,7616,7637,5301,8115,8190,5401,7605,8115,5661,8211,5661,7706,8164,5407,8249,8052,8821,8154,5314,5381,8888,7849,7763,8010,5666,5381,7986,5651,5301,8104,5301,5688,8077,5329,8762,7675,7605,7610,5301,7610,8527,7491,5413,8987,7786,5676,7616,0,5237,8179,5726,7768,8762,8179,7800,5291,5381,7605,7687,5661,5464,8816,8456,8293,7528,7548,5305,7696,7990,7763,5237,5426,7834,8795,8467,0,8093,8190,5320,5274,8316,7610,5381,5274,8387,7982,7994,8479,5248,7994,8479,9032,7946,5726,5476,5476,8201,8811,7628,8473,5496,5476,5261,8033,5564,8234,5501,8952,5381,8136,7548,7883,8915,7548,7548,5661,7706,5661,8297,7972,8504,8196,5309,7667,8467,5712,7637,5708,5453,5296,8297,7977,5480,8093,5491,8222,8174,5642,7571,5358,8670,8772,8170,7849,5320,5519,5655,5476,8994,8073,8539,8631,5564,8234,7972,7972,8297,7533,5476,7790,5480,8136,5568,5274,7877,5726,7930,8234,5237,8884,8073,9818,5564,5476,7710,7692,8504,5419,7977,8879,5305,5381,7696,7632,8613,7605,7952,7482,7774,8577,10923,10934,9345,8903,0,8653,8800,5453,5320,7977,8653,7558,5301,5676,7914,8057,5717,5476,7628,5655,7511,5688,8658,5325,5476,8201,9345,8839,8302,8658,5301,5401,5401,7692,5407,8998,7877,5358,8234,8272,8467,8093,7710,8227,5442,7888,8473,7779,8227,8456,8456,7893,8467,7632,8039,7834,8762,5237,7972,8170,7706,5325,7972,7795,8800,7935,5377,5377,5564,5248,5661,8411,5459,8653,8676,5519,5519,7849,8467,8222,8222,5564,7628,8641,7834,5296,7930,5573,5329,10940,10945,8267,8245,8131,8245,8359,8164,8467,8613,8716,7592,5612,8282,8068,7710,8940,5453,7616,5381,7542,8359,7692,5401,8222,5301,5578,5391,5329,5476,5459,5661,5274,7994,8473,8340,5325,5419,9367,8222,7521,10952,7734,5407,5622,5407,8397,5325,7883,8925,8010,8073,7521,7528,5432,7648,7800,5347,5708,7888,7952,8340,5342,8789,7877,5401,5358,5309,8903,5320,8772,5358,5347,7734,8613,7972,0,5320,8631,5391,8126,5661,8073,8033,8277,7610,5237,5274,5347,5578,5491,10961,8365,5391,5407,7533,5476,8297,8387,5237,5237,5642,8527,8772,0,8473,5564,7904,7849,5325,5325,5407,8721,8411,7834,7952,5291,7858,7790,5651,5651,7763,5391,7487,8427,7632,5291,5651,7681,5401,5642,7972,7667,5237,5261,7994,5237,5606,7628,5347,7864,8762,8196,8334,5325,8391,7616,8131,5726,8427,5309,8721,8919,5407,7632,5726,5407,7994,5291,5726,7632,5347,7914,8539,8721,5407,8721,7834,7952,5651,5391,8427,5347,5291,5325,5237,8539,5606,7628,8131,5309,5726,5285,8484,5309,8140,5291,7972,8783,5325,7786,7883,8397,8539,8987,5396,7768,5717,7710,5381,7952,7817,8062,5381,7643,5688,5426,7588,8447,5325,5329,7972,7687,5358,5426,7982,8987,7817,7972,5529,5573,5237,5476,5329,5291,5237,7834,5237,7834,7834,5693,5564,5688,5688,5285,8062,5325,8010,8329,7757,8062,5496,8272,7500,5381,5476,8504,8329,7482,5261,7706,5480,5325,8329,7482,7864,5501,8387,5642,8022,7914,7482,7528,5291,7675,8884,8033,5337,7972,7504,7565,7528,8783,5617,5274,8302,8068,7500,5476,5476,7588,7710,7849,7914,7849,7500,5655,7605,5241,8131,5209,5209,8427,7786,5564,7768,5459,8093,8539,7924,5301,5386,7533,7576,8811,5476,8170,5708,7687,5480,5476,5726,7999,5255,5358,7504,5726,7904,8245,8443,8272,7605,7839,5578,5218,7500,8329,5476,5464,7849,5391,5496,5285,5391,8727,5496,8131,7952,7768,5296,5381,5396,5671,7521,7994,7914,5442,8316,5651,8427,8391,7864,5296,7834,7504,7795,7696,5413,7681,7710,5666,7605,8582,7473,7478,7628,8909,5325,7972,8504,7588,7843,7864,8582,7864,5391,8805,7883,8073,7877,5337,7946,8255,8196,9045,8345,7628,7675,7558,5381,5274,7478,7478,5524,8022,7734,5296,5485,8851,5655,8851,8762,10969,8227,8839,5381,7990,5726,8827,7675,5342,7599,8190,7893,7710,7738,5476,8136,7482,7757,7920,8287,7877,8033,9032,5325,7706,8772,8863,7994,8170,8745,5407,5241,8115,7967,7542,5314,7779,5407,7605,8052,8099,7914,8473,8359,7858,7628,5459,7500,7858,5391,8190,5391,7643,5396,7864,7548,8539,5661,5564,7734,8805,5407,7706,7628,5209,8732,5237,5296,7482,8345,5274,7823,8033,8527,7605,7924,7834,7946,8190,5717,7495,7706,8711,7491,7994,0,8909,7967,5261,5407,8073,7692,5464,8154,5717,7616,5651,7675,5651,5237,8322,8033,9641,5377,7588,7898,7710,7542,5391,5419,8467,5661,5529,8272,5642,5642,0,7628,8473,8827,8745,5301,5305,5305,8987,5578,9476,5407,8115,5381,5401,7957,8179,7742,8721,5237,5314,5708,5708,5309,5578,5476,7982,5419,7800,8783,7653,5396,7877,7500,8833,7719,5237,5617,8217,5241,5386,5347,7864,7616,5291,5325,9021,5285,8073,5296,5622,7715,8982,7687,5391,8255,8772,8982,7734,8119,8190,5372,8772,8903,5342,7542,5325,5285,5485,5401,8417,5476,7663,8427,7920,5296,5396,5476,5476,5261,8261,8417,7542,8863,7738,7982,7576,8052,7491,7616,5407,7628,5407,5296,8762,5578,8190,8365,5666,7914,7994,7877,5301,5476,8411,5578,7834,7864,5442,7994,5442,5642,7877,8261,7482,5347,8033,8154,5578,7811,8365,5476,8903,8427,7834,8762,7834,5325,5693,5564,8504,5325,5381,8062,8272,8261,5261,7706,5480,8329,5476,8329,5325,8504,5419,5296,7742,8170,8387,8884,7500,7864,8099,7504,8022,8527,5617,7565,5291,5241,7786,7849,5255,8443,5391,7504,7687,7588,5655,8539,7967,7839,5726,7999,5358,5476,5476,8582,5666,5391,7795,5391,5381,5296,7558,7605,7914,5396,7883,8131,7768,7952,8329,5476,5241,5671,7521,5496,7643,7500,7576,5381,7478,7628,8255,7478,5524,5337,5296,8582,8851,8022,7663,8745,7605,7967,7495,8762,5314,7893,7990,7994,7628,8839,7914,7757,7877,7779,9032,5342,8772,7914,5476,8711,8033,5459,5464,5407,5661,9641,7491,7994,7864,7628,8190,7877,5209,8345,7834,8539,5651,7858,7500,5305,8903,7588,5661,5642,7898,5391,7542,5651,5309,7800,5578,5407,9476,8772,8982,8255,5347,7734,5291,7834,7542,8427,8033,7786,7542,8062,5367,5291,8062,7653,8062,5237,5237,8154,8239,7653,8762,7610,8307,5419,7706,5391,5301,8919,5509,5358,5320,5274,8527,7972,7972,7834,7834,5391,5573,5296,8068,5476,8068,5476,5407,7904,5730,7864,8140,7521,7811,5407,8800,5671,5726,5564,5237,8888,8052,7628,7605,7977,0,5237,8073,7752,7811,8888,8539,8548,5501,5476,7972,8222,7637,7653,7920,8136,7920,8131,8136,5337,7757,5305,7487,5519,7811,8653,7849,7849,5305,7653,7790,5301,8447,5358,7628,7977,8641,7790,7920,5237,7864,8170,5305,5573,8772,7811,5291,7930,5655,8467,5564,7774,7817,8594,5476,8560,5509,8751,8154,8010,8772,8721,8115,7972,7696,7605,5391,5358,5209,7696,7952,7858,8411,8349,5491,8119,8582,7893,8964,5476,7710,7790,7849,5358,5329,8504,5296,5671,7858,5209,7977,7500,9353,5285,5671,8205,5407,7616,8329,5655,5617,8888,8888,5688,5617,8732,8205,7576,8277,8365,7487,8033,8987,5606,8456,8845,5274,8349,5407,5342,5564,8772,8827,8827,7972,5325,8015,8762,5309,7504,8598,8267,8222,5437,8277,8527,8777,5325,8119,7663,5622,5564,8131,8159,8115,8239,7994,8126,8033,5622,7972,8863,8473,8267,8833,8772,8365,7834,10978,5261,5509,7616,5309,8811,5274,8205,5358,8762,5320,7834,9259,8190,7610,8811,8205,7930,5655,5564,8467,7817,7774,8115,5274,8126,8015,8154,7972,8010,8772,8751,8411,8131,7605,8721,7696,5407,5358,5391,8964,7952,8582,5491,7790,7893,7849,5476,9353,7972,7977,5296,5209,8504,5329,8205,5209,5671,5309,5688,5655,8277,5617,5564,8762,5325,5320,8987,7487,5358,7576,8845,8456,8772,8277,8527,8863,5564,5622,8159,8115,7834,8365,8267,8833,5261,7616,8811,7710,5237,5617,7823,7504,5237,7558,7849,7558,5578,5367,5629,7967,7967,5301,8484,5564,7999,8811,7706,5261,7723,8196,5274,7839,5291,8196,7710,7839,8033,8033,5459,8170,8783,8539,7719,8427,7605,8354,5726,5381,7710,5573,7795,5529,7839,7681,7888,8577,8811,5612,8140,5237,7999,7588,7482,8170,5407,5699,5629,8287,8402,5329,5726,8903,5688,5209,5377,5726,7710,5464,8255,8255,5642,5372,5377,10982,8365,8365,8443,8994,8222,7692,7990,5471,5291,5564,8033,5407,7565,7972,7730,8354,8994,7706,8354,8322,5480,7658,5476,8184,8093,5712,7904,5459,7692,5480,7977,7839,8174,5280,7605,8170,5459,7972,5564,7533,8170,5261,8073,8447,8170,5301,8682,5476,5325,5491,5274,5285,7888,7605,5622,7473,5635,7952,7723,8005,7521,7710,5329,5413,7972,7977,7681,7774,5407,7738,8255,5358,5274,7888,5325,8170,5325,5320,5647,5573,5485,8077,7675,8073,7710,8658,8711,5320,7843,5218,5285,5442,8255,0,7738,5407,7706,8494,7994,5314,5381,5325,8077,8170,8170,8099,5329,8873,8359,8170,7710,5509,7768,5377,8073,8443,7867,5325,8140,8548,7774,8548,5352,7972,5241,7828,7972,7628,7888,5471,8104,5218,7548,8539,5578,8033,8663,5407,7795,5629,8170,5564,8443,5459,7858,8110,7495,5377,5496,8422,5407,8479,8451,5237,7738,8322,8427,7828,5377,5325,8077,7800,7511,8427,5358,5248,5329,8239,8140,5629,5491,5612,5237,8005,5325,7972,8322,8539,7800,8548,5401,5413,5372,8184,5432,8422,8170,8919,8554,5274,8010,5573,7548,5471,8073,8140,8443,5291,5291,7516,7738,8255,8255,7734,8239,5471,8498,8582,5372,7738,5358,5358,7605,8170,5573,8255,5471,5442,5325,7478,8354,8184,7516,8577,7972,5301,7972,7877,5573,8676,7994,7610,7516,5274,7994,8857,5347,5661,8261,5661,8077,7738,5726,5381,8498,8857,5237,8005,5291,7516,7914,7817,7883,10986,5726,5377,7888,7888,7817,7843,10994,7632,7663,5325,8170,5629,11001,11009,7834,11013,8756,5726,7957,8149,5480,7972,11023,5377,8479,11023,7628,7935,8387,7952,7667,7994,8354,7528,8539,8190,8010,8504,5261,8322,5320,8068,8033,5491,7864,8136,7706,7790,8821,8504,8010,5622,8062,7982,5726,5464,8539,8093,5491,5708,5655,8010,8582,8608,5352,7930,7786,7692,8174,7605,8164,7864,7904,7576,7972,5642,7990,5476,8745,5391,5255,7696,7723,8170,5285,8687,5329,8509,7768,7576,5437,7972,5617,7779,7628,8670,5337,7811,8267,5578,5509,5564,8745,7576,8447,8093,5578,8427,7779,5209,5296,7599,8287,8039,8795,5476,5352,7542,8234,8184,7858,8721,7930,8099,5213,7779,8821,8110,5325,7924,5274,8732,8694,8658,5717,5337,8062,5320,5564,5209,5564,7752,8543,8427,7628,7752,8272,7786,5655,5367,8467,5329,8484,8783,5622,7994,7643,10830,7935,5285,8427,7734,8149,5501,8010,7516,5717,7719,8795,7811,5476,5325,8217,7696,7628,5274,8062,5666,8456,5301,5301,5329,7516,7935,8387,7952,8190,8539,8504,5622,7864,8099,8821,8062,7692,7990,5329,7576,7972,5476,7982,5655,5301,5642,8745,5391,5437,7972,5285,7576,7768,5329,5285,5578,5564,7779,8184,7930,5296,8234,7599,7779,8287,5209,8694,5564,5325,7752,5320,8456,8467,5367,8427,5329,7935,7516,7528,7786,7839,7839,7920,8397,8411,7986,7986,7839,5529,8282,8411,8411,8411,0,8411,8282,7839,8411,8411,8033,8613,8789,8789,5655,8287,8287,11031,8705,8411,8411,11039,11044,8239,8245,7877,8811,7542,7628,7628,7706,8329,5476,8039,8349,7706,8925,7599,7977,9032,7616,5377,8267,5309,8560,8387,8010,7478,8245,7864,7811,5578,11048,5309,8467,7994,5401,7710,7935,8447,7681,7972,5476,7592,7977,8033,5491,7696,8539,8312,7599,5459,8115,8272,5320,5480,11056,5485,5296,8316,5337,5651,5413,5325,8716,5476,5381,8863,8577,8329,8422,8022,7795,8272,8334,5730,8447,5471,7592,8190,8329,8201,5337,7675,5688,7853,7605,5688,8539,8539,5647,5485,5377,8307,5476,5578,8316,8687,7478,5391,7558,7588,5372,8658,8527,5401,5612,5296,5301,11061,8277,8543,8451,5291,5413,7811,5491,5329,5337,7576,5309,7628,8287,8005,7994,5377,5476,8245,8467,7632,5358,7495,7628,8903,5342,5471,5342,7834,8732,7710,5377,7628,5391,5655,5337,8267,7864,8411,8039,7864,5367,5464,5688,8479,8245,7994,11067,5476,8131,5642,5573,7511,8329,7738,8539,7710,8467,7588,7738,7542,0,5642,8721,8015,5708,5391,8604,7710,7588,5372,5578,5471,8539,7957,7710,8451,7516,9021,7648,7542,8190,7628,5476,7478,8239,8170,5391,5358,5642,8349,5661,5342,7811,8427,8909,7696,7622,8411,8411,7986,5301,8467,5642,7632,5301,5476,8805,8919,7706,5218,5261,7653,7628,8211,8613,7723,0,8582,5564,8756,8888,7565,5426,8136,7528,7914,7898,8467,8879,8154,5661,7898,7478,8170,5337,7653,7658,5688,5358,8456,5224,8670,7930,8631,8539,7967,7491,7516,8582,7952,7605,5391,7692,7628,5671,5717,5237,7972,5325,8329,5730,5426,5285,5237,7952,7495,7843,7977,7908,5241,11075,8267,7834,7558,8307,7710,5329,5564,7478,5564,5524,8052,7843,7977,8845,7667,7616,8073,5237,7930,5301,8267,8201,7675,7706,7920,8745,8039,7576,8772,7967,5255,8277,8447,8033,5337,8201,7521,7883,8170,5218,8255,5476,8762,7834,5712,8893,7930,7864,7628,5676,7628,8267,5730,8005,7521,7521,5448,5629,8391,5237,8613,5573,5209,7967,8170,7478,7521,8267,8052,5218,5358,5305,8783,5564,8239,7482,5622,7982,8255,7834,8015,8010,8255,7500,8919,5358,7528,5358,8554,7994,5241,7790,5261,7516,8312,7883,8249,7491,8340,7908,8527,8249,7967,8582,5401,8365,7877,5337,7706,7834,7877,8365,8093,7811,5237,8527,8365,5301,5337,5693,5688,5314,8456,7576,5476,8560,7628,8484,7834,8397,7790,7883,7500,7853,5655,5501,11082,0,8267,7658,5606,5280,5480,7571,7653,7786,7588,8312,5442,5437,8391,7632,8287,5442,5337,8316,7972,5274,5419,7558,7482,5647,7675,5712,7692,7940,5666,7734,8397,8370,7834,5237,8929,7994,5329,5347,7834,8272,8456,8427,8104,8772,8397,8287,8140,8653,5377,7746,8762,7982,7500,5426,8772,8484,8104,8267,8312,7990,5448,5651,7768,5442,8451,8267,5218,8608,7687,8370,5629,5419,8647,5261,7628,5301,5372,8149,5476,5419,7734,5285,5237,8772,8312,8789,5476,5301,8170,5285,5476,8929,8613,8329,5442,7834,8093,5651,5651,8929,7528,5285,5237,5651,7834,8411,7774,5391,5337,5391,5391,5301,7548,7491,7779,5301,5309,5309,5688,5476,7757,8479,7999,5693,8312,5688,7849,5564,5655,7972,7904,8375,5564,8716,7757,5261,8255,5568,8732,5209,8010,7628,5396,7653,7817,7706,8211,8915,8539,7487,5237,5476,7637,5391,5647,5237,8293,7542,11094,5407,7920,7491,7482,7904,7565,8033,5426,7853,8354,7834,5564,7920,8136,7528,7864,5291,5471,8131,7482,7542,8282,5325,8504,8783,8816,5501,8267,5325,8196,7920,8039,8062,7482,7667,5391,5476,7548,8077,5671,8915,8154,7658,8756,11104,11112,11119,0,7888,5209,8447,5647,5391,8438,7786,7864,8093,5224,5476,7786,7904,7920,7628,7487,7571,8201,5337,7828,7977,5655,5391,5391,8964,8170,8427,5391,5617,8190,7999,8222,7849,5358,8119,5573,5391,8073,7605,7710,8925,8312,8316,7542,5564,8533,7511,7542,5564,5301,8354,7920,7924,8282,8073,5381,8131,5320,5241,7715,8447,8077,8539,7977,7715,7883,7914,5476,7487,8582,9012,7930,5407,8316,5426,8062,5285,7710,7696,5717,5224,5391,8711,7977,7482,7667,7576,5248,8987,8316,5305,5666,5325,7628,7500,8504,9021,7972,7723,5261,7710,5274,5426,7768,7920,8964,7605,5730,8391,7473,5642,5274,5274,5519,7667,5496,8149,5325,7920,8613,5651,5476,7482,8687,8345,9562,5337,7719,8898,7542,8375,11126,7663,7757,7653,7872,5391,8946,5688,7675,5578,5519,8205,5377,8451,7795,7817,7548,8307,5329,5712,7843,5509,7528,8267,7930,7478,7588,7628,7877,8805,8073,7653,7977,8062,8498,8527,7877,5509,7972,7487,8190,7478,7478,7681,7817,5314,9471,5426,7542,7914,7653,9471,5661,5573,5241,8170,5274,8964,8211,7734,5301,7533,7872,7558,5524,5301,8594,11135,7605,11142,5647,7511,8397,8170,8227,8467,5407,8461,7757,8272,8227,8099,7473,8093,7930,5329,5301,8322,8057,5676,8438,8397,5352,8345,7920,5274,8387,8863,5666,9051,8571,7738,8653,8762,7994,7805,5209,5241,8543,8077,8827,8658,8772,5693,7706,7542,5314,7706,7628,5309,8721,7790,8227,5606,5237,5401,8272,7786,8451,5320,5655,5329,8249,5329,5396,8473,5205,8277,8800,11147,8201,8716,5509,8365,0,8407,5274,0,8154,7779,5933,8514,8509,7616,8539,5401,8039,5274,8762,5647,7994,8827,5564,8110,5325,7864,8110,8033,5377,7687,8062,7924,8676,5367,7628,8946,5426,5464,7994,5564,7999,5496,7867,5617,8062,8005,5606,8711,8255,8255,5730,7853,8827,7811,7491,8438,5407,8676,5407,8022,8005,7982,8190,7888,5381,5655,8411,7548,5209,5519,7491,5712,5213,7663,5381,0,5712,8287,7738,7898,5367,5401,5612,5237,7511,8443,7491,8417,5534,8164,5629,5612,8322,5717,8851,7805,8073,7533,7730,5358,8940,8461,5209,7478,7616,5464,7681,7511,8509,7962,8302,5448,5509,8052,5655,5655,7632,5476,8467,5629,8077,5237,7977,5717,7538,5676,11126,11104,5612,0,0,0,8136,5329,5612,7742,8443,8641,5519,8721,8190,5401,8087,5708,7800,5305,7632,5358,7994,7511,8539,8255,8827,5476,5329,5708,8946,8322,8402,7706,8227,5629,7800,5218,7877,8527,5485,8473,5320,7957,5519,8827,7610,8365,5407,8340,5301,5367,5237,8756,0,5413,0,0,5241,5329,5396,5325,7734,5296,8322,8312,7516,5612,5519,8589,7558,8005,7487,7511,5386,7946,8959,5342,8010,8255,7768,7924,5629,7521,5291,5285,9021,8239,5381,8149,8217,8772,8365,5325,5407,7548,8919,8387,7616,8217,5291,8417,5377,7734,8964,0,0,5693,5261,8509,5407,7893,5342,5329,5676,5296,7628,5401,5314,8255,7542,5314,5666,5358,8964,7811,7491,7888,8039,8903,8170,5501,8245,7930,5476,7663,8548,8329,7757,7605,7946,8863,5325,8077,7738,8154,8745,8888,7616,8005,8381,7500,5407,5407,11155,5496,7972,7763,5301,5666,5325,8857,8582,8987,5352,7986,8762,8851,8287,5329,8473,8222,5329,0,0,7610,7864,5329,7528,5407,5347,8073,5407,5642,7877,7521,5407,5237,8721,8777,8077,5666,8498,5726,7533,7632,8857,8375,5372,8077,5291,5629,5320,5347,5309,5688,5476,5655,5693,7999,8312,7757,5647,7542,8732,5261,5325,8010,7757,8514,8293,5237,7637,5426,5671,8201,7658,8170,5471,8783,7548,8136,7930,8461,8282,7542,5642,8196,7667,7478,8131,8756,7565,8154,7853,8816,7904,7628,7542,5241,7542,7786,8073,7864,8312,8201,7904,8354,8857,7715,7883,7977,7828,5337,8964,8073,8093,8851,7542,7511,8222,5391,8190,5617,5358,8447,7920,7888,5426,8898,8149,7696,8211,8387,5337,7558,7977,7491,5386,7972,8903,8316,5329,7757,7482,8509,7687,7605,5274,8805,9003,5274,5396,7768,8302,7977,5642,5717,8391,5241,7719,5285,5476,7877,5248,5629,8345,7482,8613,7605,8149,7734,7478,8277,8594,5401,8946,7675,5612,5301,5241,5573,8039,5524,8964,5519,8451,9471,8527,7528,7511,8509,5407,7872,5377,7843,7542,8170,9051,8322,7888,5606,8451,7790,8543,7706,8407,7473,8653,8227,5237,7786,7632,7805,8863,5309,7972,5274,5676,8154,8057,8772,5693,5407,8170,8249,5209,7867,7616,8509,8322,8022,7864,5496,8255,7548,8676,5448,5464,7811,7663,8762,5712,7957,5209,7893,5712,5305,5655,8073,5717,8077,5629,5676,8077,8136,7511,8052,7898,5476,7977,8888,8164,8473,5629,7800,7632,5413,5218,5485,8539,8443,8190,7511,8772,5291,8239,7734,5329,8217,5347,7734,8498,8322,8919,8154,5666,7811,5314,5401,5476,5496,8473,7986,7877,7521,8227,8227,8484,5708,5459,5726,8239,5651,8227,8514,7538,8969,5261,8154,8514,7834,5358,7653,5358,8589,8509,8756,5413,8062,8489,8589,5274,5274,5407,8438,5367,11161,8307,7687,5386,5407,5352,5367,8093,8411,5358,8489,7849,8721,5717,5717,8048,5573,8062,7952,8438,7719,8653,7843,5329,8805,8302,8170,8354,8354,7877,5726,7516,8227,8635,8653,7834,8438,7834,7864,7643,7628,7752,7521,8222,7521,5726,5642,7752,8282,8345,7994,7482,8144,5699,5347,8762,0,11167,5606,8077,7738,8509,7849,5699,8519,8721,8307,7706,8005,5629,8721,8647,8795,5237,5296,7877,7920,8903,7811,7738,7752,0,8514,8969,5261,7834,7653,5358,7752,8519,8489,7994,5274,8062,5407,8438,8756,8514,7687,5386,7952,7752,7738,8438,5329,8509,8048,5717,7849,8653,8484,8354,7843,8762,7628,7834,8227,5237,7752,7643,7834,7864,7877,5347,5726,5296,8144,5699,5717,5606,7738,5699,8795,7811,5564,5564,5333,8217,7528,7542,7478,5476,5480,7482,7924,8307,7864,8119,8282,8456,8946,8267,8821,7972,7920,5485,7565,7924,7482,5655,5381,7558,7864,5726,8539,8443,7571,7828,8093,9051,5480,5480,8312,5224,5564,5325,5329,7610,8033,5426,7888,7957,5305,5419,5320,7888,5274,7696,7952,7628,5671,5261,8329,8255,5426,8104,5564,5358,5367,5261,8119,7972,7734,8307,8467,5655,5573,5274,7853,8077,8267,5274,8456,8456,7967,5476,8217,8099,7482,8170,7805,8033,7920,8093,8272,5274,5329,8119,5274,7482,7605,7924,8705,5261,5248,7834,7482,7628,5377,7628,7610,7994,7994,8077,8217,5342,8345,5464,5381,5342,7482,5671,7834,5296,5717,8267,7478,7994,7663,5237,5629,5325,8015,8479,5708,5237,7482,11175,7648,7516,8422,8919,7920,5342,5342,7628,5274,5501,8447,7696,5325,8479,5237,7482,7864,5296,7610,5237,5301,5301,5301,8863,7849,7972,9032,7667,7823,5476,5699,7834,7542,8387,7504,8387,5325,7786,7811,7972,8131,7667,7839,5218,8170,5573,7982,8925,7632,7628,5442,7757,5342,7653,8154,8617,8721,5573,8925,5218,5325,5237,5442,7511,5301,7823,5529,8925,5325,5325,7628,7628,5529,8136,8998,7719,7715,7752,5501,8334,8267,5301,5301,8608,8093,5666,8345,8222,8249,7828,8903,5426,7478,7999,7500,5325,7715,8033,7675,7687,8805,5655,8456,5712,8093,7706,5442,8154,8302,7893,5534,8427,7482,8447,5442,8068,7516,0,7582,7616,7478,8093,8617,8093,5367,8062,7482,5305,8349,8267,7511,8249,8888,7478,7994,8154,7482,5237,5476,7663,7883,8919,5274,7628,5329,7610,5491,5325,8334,7924,7904,7920,7994,0,5237,5325,8349,8249,8249,5666,5301,8154,7663,7893,5491,7610,8093,7663,8527,5301,8222,0,7849,7849,5224,8745,8745,5485,5606,5655,5485,7558,8745,5485,5485,7828,5485,5606,7893,7893,8898,5651,7746,7746,8174,7491,5606,5296,7491,5717,5693,5496,5407,8504,8539,8762,7478,8504,8196,5309,5529,8816,7904,7482,7730,8136,5305,8443,8245,8282,5476,5642,8272,5480,7977,8062,8174,7924,11180,7482,7977,8272,5325,5661,8345,8345,8727,7817,11186,8245,8255,8805,5509,7616,5342,7681,8255,8190,8571,5413,7738,8015,5224,8144,8170,7986,5661,8110,7898,5476,5688,5688,7849,7675,7715,8170,5407,8411,8539,5407,11194,8272,8131,7738,7930,5661,7533,5717,5358,8144,5629,7957,7952,8255,5529,5325,5407,5419,7877,8245,8154,5274,7542,8762,8631,5407,5347,7994,8504,5564,5712,5676,5717,7994,8255,7653,8227,0,8110,8329,5617,7994,5717,7930,8272,8131,8267,0,5358,7994,5296,8762,8631,8762,7994,8504,5564,7653,7994,8267,8272,8484,8484,5274,5661,5274,5325,7482,8925,8267,8670,5333,5352,7946,7616,8267,5352,7482,7482,7521,7982,7491,5693,8745,7533,7521,8598,7653,7500,5476,7500,7786,8022,8005,7706,8560,7628,5509,8282,5471,8539,7883,7533,8312,8093,8604,5485,5666,7849,8255,7864,7864,7994,5381,7795,7914,5476,7914,8370,8217,7858,5255,5666,5564,7977,7853,5296,8005,7977,5333,5606,7558,7616,7667,7982,7653,5485,8422,8422,8751,8277,8422,5209,5209,7805,5693,8277,7834,7653,5407,7990,8402,7592,8196,5476,8159,5377,7864,7864,7834,7643,8795,5218,5622,7834,5274,7528,5476,8467,8940,7828,7828,5419,7692,5666,8239,8647,5476,7800,7706,7648,8052,8255,7786,8329,7542,5296,8427,8613,8062,8888,5274,8751,5329,5347,8077,7768,7491,5693,8745,7653,7521,8598,7500,7500,5509,7786,5471,7706,8282,8022,8560,7628,5329,8093,8312,5485,5381,7977,5606,7653,7616,7592,8370,5476,8751,5296,8422,7653,7616,7616,7990,5377,5218,7864,5622,7834,7864,8077,7800,7828,8940,8052,5666,8427,8613,8751,7768,8527,8473,11198,5564,5367,8427,8473,7730,7904,7658,8577,7795,8190,8670,8170,5358,7487,7994,7924,5642,5448,8509,5514,8010,7511,5237,8239,8647,8647,7511,8473,8473,8473,8239,0,7719,8527,8473,8527,7924,8427,8473,7730,8170,7487,5448,5642,7511,8647,8473,8473,7893,8010,7893,7893,5391,5391,8179,5325,5693,7487,5480,8427,7962,7521,8068,8504,7628,5676,7710,7482,8010,8010,7962,7482,7605,5476,8533,5358,5381,8272,7511,7839,8964,7487,8222,5391,7888,8370,8504,5606,5285,7977,5224,5642,8316,7839,7521,7924,7924,7696,7834,5333,5476,8179,8073,8028,7864,8201,7667,8307,7588,7628,5391,5642,7643,8039,5391,5407,8863,5224,5274,8201,7491,8772,7893,8015,7994,8721,7864,8699,7643,7982,5464,8964,5296,5407,5730,7849,5426,5501,7994,7786,5496,8022,5629,8073,5524,5237,8005,7511,5448,8272,7491,8267,5305,5629,7800,5358,7628,5519,5309,8589,8005,5622,5261,7710,7883,5476,5325,7648,7558,5218,8272,5296,7521,8427,5501,8370,8249,7834,5305,7877,7521,7632,8857,7700,5391,5693,5325,5480,5426,8068,5237,8504,7628,7710,8010,7482,5407,5391,8222,7487,7888,5476,7696,7648,7521,7977,5333,5285,5476,5224,8073,8307,7864,7628,8028,7667,8039,7643,5274,8721,5496,8005,7786,5448,7877,7491,5305,8073,7511,5519,5309,7800,8589,5622,7700,8087,8005,8039,7521,5501,7904,8447,5358,8073,5647,7653,7893,5407,8345,7663,5564,5274,7752,7528,5501,8888,5426,7538,7628,8527,7653,8539,5480,5480,7710,8329,7605,7972,8456,7528,8915,7730,7482,7752,7972,7883,5329,5337,8073,8222,7908,5480,5476,5726,0,5726,7592,8073,7999,8467,5352,7576,8170,7605,8293,8170,7920,8205,7977,8093,5655,7658,5564,7924,5391,5358,5480,5480,7487,7511,7538,7858,8272,7972,8164,8909,7582,8329,7768,7914,7592,7696,8370,8073,7605,7482,7632,11206,7883,5661,7511,8170,8022,8196,8211,7779,7653,5647,8190,8422,7667,7864,7558,7528,7663,5329,8863,7576,5617,7858,8653,8272,8277,8548,7990,7706,7706,7834,7893,5514,7834,5381,5642,7478,7858,8827,7858,7542,7893,5464,5407,8154,7628,5377,7768,5367,8548,8005,8110,7487,7795,5480,8005,5448,7542,7558,7972,7511,8334,8909,5237,8340,8762,7834,8467,7967,5514,5261,5301,7972,8617,5329,5237,7632,8015,5305,5708,8179,8647,8827,5726,5358,7972,7628,7500,5396,8073,7588,7675,7516,7877,5471,7511,5480,8833,8772,5285,8964,7834,7632,8427,5485,5476,7491,5480,8888,7967,7834,5352,5329,8909,7521,5325,8617,8170,5237,7811,5301,8473,7538,7628,5480,8329,7710,5726,8915,8073,5329,8205,5391,7487,5564,8170,7967,7605,5480,8293,7977,5476,7658,7675,7538,7482,5285,5396,7632,8370,7858,8473,5301,7558,7834,7528,8196,7667,7706,7706,7990,8863,8548,7588,8179,7858,7972,8005,5514,7478,5708,7511,7542,5261,8647,7632,8827,7877,7967,5325,8617,7521,7786,7994,7994,7994,7628,8811,5476,8608,7853,5661,8222,5358,7710,5377,8539,7692,8370,8255,8131,7473,5717,8422,8845,7858,5358,7990,9012,7628,5342,7957,8073,7675,8334,8073,7839,8739,8888,5305,8239,7957,5386,8762,8205,5342,5377,8888,8739,5329,5524,5524,8255,8898,8255,8149,5372,8473,8653,8653,5377,7565,5519,8297,7790,7883,8653,8888,8048,7681,5352,5209,8048,8297,5377,8174,7920,7790,5564,7696,8164,5726,7817,5519,7795,5325,5352,9361,8670,8302,5377,5301,8888,8548,5377,5352,8022,5241,7768,8548,8598,5407,7888,7982,7986,8484,7706,5401,5655,8888,5352,5305,8589,7800,7800,9026,7500,8577,7542,7542,8789,7811,7908,8381,8888,8577,8261,8307,8307,5386,8062,5237,8307,7653,8307,8307,8484,8227,7628,7628,5301,5676,5564,7542,7952,8548,7511,7952,8987,7628,7952,5712,5325,7706,5381,8005,7643,7904,8312,5712,7500,7834,7675,7779,7628,8422,7706,7599,7779,7994,5407,7710,8647,5358,5305,5325,7500,5442,7834,8427,7628,8479,8504,5325,11212,7817,5480,7757,5325,7500,7864,7864,8302,8560,8068,5305,5381,7914,8010,8467,5358,5329,7752,7982,5329,8925,7565,5501,8136,5337,11217,11225,7849,5367,5726,7653,7920,8093,5480,8073,5688,5255,5564,5358,5325,7994,8170,7757,8073,5337,8234,7920,8184,5426,7658,8222,5564,7687,8170,7786,11231,8316,11239,7738,5381,5320,5730,7521,7696,8345,7994,7681,7605,7482,8577,7632,8316,5476,7706,5224,7994,5285,8201,7952,5274,5717,5296,11243,11249,8898,11255,7888,7667,5377,7800,7675,8925,8302,8196,7628,8028,5578,8234,8845,7588,7478,5688,8805,5564,5509,5337,5301,8302,8302,5524,8249,11261,11267,11274,11285,5218,8447,5209,7706,8745,7521,8028,8104,5325,7757,7867,7786,7576,7582,8277,7893,8447,5476,8653,5329,8484,8227,5485,8099,8093,7967,5301,8249,8548,7972,8184,11295,11303,11310,5391,7914,7643,5391,5730,5426,7605,7986,8548,5325,8411,8411,7811,7768,8115,7994,7994,7628,8658,8110,5459,5464,5401,7834,5688,5688,5407,5358,7864,7924,5564,8159,5407,5367,5480,7982,11319,8893,11328,7752,5407,8052,7478,7565,5514,7511,7538,5391,5391,7482,11336,8909,7738,5642,5717,8164,7542,7706,7752,8893,11339,5401,5708,5333,5296,8438,5325,7681,5726,5407,5514,7800,5237,5688,8473,5325,5325,7605,5419,5688,5622,5218,8015,8756,5529,5432,5291,5726,7521,8073,5285,8329,7588,5464,9021,5261,5261,8010,5291,7516,8329,7622,5237,11349,11354,5501,8721,5730,7908,9353,5514,8427,8811,7952,8119,5301,8227,11339,11360,11368,8613,5325,7500,8154,11360,5301,8577,5301,7763,5655,7521,7864,5329,8721,5301,5274,7628,8479,5325,5337,8068,5329,7565,7904,7849,8073,5255,8184,5329,5337,7687,5564,8073,8222,5730,7920,7658,7952,5381,8898,7994,7696,8316,8115,5730,7908,5325,5285,5274,8925,7521,8329,8845,5301,5401,5407,5301,5391,7681,8234,8302,5509,7667,5325,5218,8745,8093,7706,7967,7893,8653,8227,7786,8447,8184,7757,5485,7914,5391,5209,5501,5459,5358,8227,5391,7752,7994,8159,7864,5688,5564,5464,7768,5419,8411,5514,5708,7706,7738,7482,5642,7565,5407,5333,8473,5529,5432,7800,8438,5218,7994,5237,5291,5261,7516,9021,8154,8811,5301,8427,8721,5453,5476,5564,5301,8484,8201,7834,5564,7757,5325,8527,11375,8811,5391,8527,8391,8222,8467,7972,7982,8196,5564,5501,7752,5407,5391,7628,8998,11380,5291,5291,7920,7811,5655,8222,7834,8387,5241,7565,8527,7752,7742,8594,7977,8293,8093,7763,5564,5480,7752,5301,8411,7972,5717,8467,5209,5726,7675,7972,7883,8170,5337,7786,7548,7628,7924,8164,8387,8964,7628,11386,7616,8863,7478,7696,7795,7663,7972,7834,7946,5285,5325,8577,7605,8504,5476,8062,7632,8613,8711,5717,8504,5285,5519,7576,7972,7632,11393,11402,9457,8375,7977,5325,7628,7811,5480,8201,7478,5676,7628,8073,8329,8329,5358,5237,7667,8170,5647,5485,5377,7864,7864,8582,7982,7478,7914,8277,11408,11417,5329,5255,5320,5564,8345,7999,5352,8196,7542,7930,7757,5329,5699,5407,8170,5647,8387,8467,7706,5301,7491,8863,8571,8863,8653,8119,7706,5485,5476,5476,5485,5209,8277,7582,7706,8104,8845,8170,5726,11425,11432,5325,5320,0,7710,7622,8110,8345,5688,5377,7982,5377,7864,5381,7790,5564,8104,5647,7834,7849,8467,7858,8267,5712,7982,7616,5471,7715,8170,11445,8777,7565,7786,7616,8467,8190,5534,5391,7849,5573,5629,5367,8893,7511,5325,7786,5407,9032,7653,5209,5209,8447,5642,7834,5325,5301,5606,5568,5377,5377,8447,5647,7538,5285,5524,5606,8033,5301,5622,5578,5708,5642,5476,7742,7710,7972,8239,8164,7957,5358,8617,5314,7628,7478,5291,7482,7768,7487,5285,5476,5396,5358,7616,7588,5464,8010,5285,8239,7834,8898,8484,5274,5274,5647,8349,9021,7628,7616,5329,8641,5274,5529,5476,7920,5320,7632,7706,5476,5325,5578,7628,8427,7491,7924,7920,8381,8154,5651,7616,8249,8539,8354,7628,5666,8222,5329,7849,7610,8617,8354,7616,8721,5726,5301,8375,5453,8484,5325,8467,8391,5391,7742,8387,8777,8222,5655,7786,7582,5329,8387,7675,7924,8093,7972,5726,8467,5529,5480,7487,7972,7696,7795,5519,7946,7576,5717,8375,8062,7478,8073,5301,5676,7982,7864,7628,5274,5377,7478,5699,7622,8345,8653,7930,5352,7542,8104,8845,8467,7588,7982,7849,7864,7786,5688,5209,5712,7715,8447,5642,5534,7511,5325,8447,5407,7849,5476,7616,5578,8239,8239,5285,8484,7628,5329,8190,8427,7616,8154,8249,8721,8617,5329,5309,8093,5407,5274,8451,5407,5407,7834,8451,5329,5337,8140,5325,8473,8140,8473,7605,7667,7605,5407,5651,5651,7478,7511,8033,7667,7516,8447,7706,5329,8484,7667,5485,5301,7521,7834,7977,5651,5301,5617,8015,7516,5485,5325,8093,7834,8140,8349,8349,5717,8783,5564,7746,7746,5564,8174,5726,5726,8711,5564,5274,8277,5726,7746,0,8527,5564,5726,7746,7538,5305,5305,5296,7538,7967,5362,5501,5464,5464,5309,8687,5606,7853,7946,5237,8582,7946,5320,5301,8184,7710,9372,9372,5476,7542,7883,5237,8641,5305,5305,7542,5491,5573,5337,7828,5476,5367,7834,5726,5712,7834,5485,7628,5301,8903,8762,8179,7482,8345,7834,8772,8345,5655,5491,8179,5476,5712,7883,7834,8762,5329,7972,5501,5564,5564,8438,8438,8467,8140,5726,8783,8631,7605,7972,8211,5708,5432,5480,5358,8467,8783,5480,5693,5651,8716,5693,5209,7786,7786,7582,5501,8272,8467,7920,8227,5524,7706,8467,5629,8909,5320,5320,5501,7893,7752,7904,7757,5480,8316,5726,8119,5391,5337,5391,8805,7478,8184,5485,7643,8170,7834,5647,5377,5237,5274,7834,5666,5358,5642,5688,7653,7478,7478,5730,5237,5717,7687,5519,5534,7687,8789,7700,7706,8533,5325,5209,5325,5325,7706,5325,7710,8033,8033,7849,8387,8800,5661,8174,8451,5391,5209,7710,8184,8170,7681,8093,8093,5573,7768,5661,8800,8077,8484,5642,8287,8653,7628,5573,5476,8447,8451,7967,5726,5362,5274,7742,7864,8494,5476,8287,7967,7828,5573,5309,8387,7710,8033,7849,8800,8170,8093,8174,7681,5209,8800,7628,8287,5726,8494,7610,8334,7817,8334,7834,7610,7610,7817,8795,7738,8093,7738,7610,7817,8795,7952,5688,5432,7952,8354,7805,7849,5291,5661,7628,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4269,11454,0,0,11459,0,0,0,11464,0,0,0,0,0,0,0,11469,11474,0,11479,0,11484,0,11490,0,0,0,0,11495,11500,0,0,0,0,0,0,11506,0,0,0,0,0,0,0,0,0,0,0,11512,0,0,0,0,0,0,0,11519,0,0,0,0,0,0,0,0,0,0,0,11525,5599,40,11528,11531,11535,335,11539,4899,11543,24,11546,11549,11553,11556,380,11560,52,11563,11566,32,3103,11569,11573,2795,11577,11581,11586,11591,11595,11600,11604,800,11608,11612,11617,11621,11626,11630,2809,11634,11638,2806,11642,11646,11650,2792,9676,11654,11659,11663,11667,11671,11674,11678,11683,11687,11691,3457,11695,11699,11704,11708,11713,11717,803,9686,11721,11726,11730,11735,11739,3471,11743,11747,11751,3454,11755,9690,11759,11763,11767,11771,11774,11778,11783,11787,11792,11797,11801,11806,11812,11818,11823,11829,11834,1073,11839,11844,11850,11855,11861,11866,11871,11875,11880,11885,11889,11894,11899,11904,11908,11913,11919,11924,11929,11934,11938,11943,11948,11953,11957,11962,11968,11973,11979,11984,11989,9710,11993,11998,12003,12007,12012,12017,12022,12026,12031,12037,12042,9721,12047,12051,12056,12062,12067,12072,12077,12081,12086,12092,12097,12103,12108,12113,12117,12122,12128,12133,12139,12144,12149,12153,12158,12163,12168,12172,12177,12183,9740,12188,12192,9745,12197,12202,12206,2599,12210,12214,12219,12223,12228,12232,845,12236,12240,12245,12250,12254,12259,12263,2613,12267,12271,2610,12275,12279,2596,12283,12287,12292,12296,12300,12304,12307,12311,12315,1733,12319,12323,12327,833,12331,12335,2759,12339,12343,12347,2738,12351,12355,12360,12364,12368,12372,12375,12379,12383,2819,12387,12391,12396,12401,12405,12410,12414,985,12418,12422,12426,2833,12430,12434,12438,12442,12446,2816,12450,12454,12459,12463,12467,10579,12471,12475,12480,12484,12488,3120,12492,12496,12501,12505,12510,12514,816,12518,12522,12527,618,12531,3194,12535,12539,432,12543,12547,12551,3183,12555,12559,12564,12568,12572,324,12576,12580,5230,12585,12590,12594,806,12598,12602,12607,12612,12616,12621,12625,2854,4993,12629,2851,12633,12637,12641,2840,12645,12649,12654,12658,12663,2496,12668,12673,12679,12684,12690,12695,942,12700,12705,12711,12716,12722,12727,2514,12732,12737,2510,12742,12747,12752,3201,12757,12762,12768,12773,12778,10015,12783,12788,12794,12799,12804,10060,12809,12814,12819,10264,12824,12829,10330,12834,12839,12844,10207,12849,12854,12860,12865,12870,12875,12879,12884,12890,12896,12901,12907,12912,3064,12917,12922,12928,12933,12938,12943,12948,12953,12957,12962,12967,12971,2950,12975,12979,12984,12988,12993,842,12997,13001,13006,13010,13015,13019,1400,13023,13027,2957,13031,13035,13039,2947,13043,13047,13052,13056,13061,13066,13070,13075,13081,13086,13092,13097,4878,13102,13107,13113,13118,13124,13129,13133,13138,13143,13147,13152,13157,13162,13166,13171,13177,13182,13187,13192,13196,13201,13207,13212,13216,2547,13220,13224,13229,13234,13238,13243,13247,839,13251,13255,13260,13265,13269,13274,13278,1426,13282,13286,2554,13290,13294,13298,2544,13302,13306,13311,13315,13319,4493,13323,13327,13332,13336,13340,3228,13344,13348,13353,13358,13362,13367,13371,915,13375,13379,13384,13389,13393,13398,13402,3242,13406,13410,13414,3239,13418,13422,13426,3225,13430,13434,13439,13443,13447,2998,13451,13455,13460,13464,13469,13473,836,13477,13481,13486,13490,13495,13499,1311,13503,13507,13511,3009,13515,13519,13523,2995,13527,13531,13536,13540,13545,3271,13550,13556,13561,13567,13572,1060,13577,13582,13588,13594,13599,13605,13610,3289,13615,13620,13625,3285,13630,13635,13640,3267,13645,13650,13656,13661,13667,13672,13677,13682,13686,13691,13697,13702,13708,13713,13718,13722,13727,13732,13736,13741,13746,13751,13755,13760,13766,13771,13776,13781,13785,13790,13796,13802,13807,13813,13818,13823,13827,13832,13838,13844,13849,13855,13860,13865,13869,13874,13879,13883,13888,13894,13899,13905,13910,922,13915,13920,13926,13932,13937,13942,4136,13947,13952,13957,13961,13966,13970,1623,13975,13979,429,13983,13987,13992,13997,14001,14006,14010,2537,14014,14018,1620,14022,14026,14030,852,14034,14038,14043,14047,14052,3114,14056,14060,482,14064,14068,14072,3126,14076,14080,14085,14089,14094,14098,1044,14102,14106,14111,14115,14120,14124,3137,14128,14132,464,14136,14140,14144,3123,14148,14152,14157,14161,14165,14169,14172,14176,14181,14185,14189,2864,14193,14197,14202,14207,14211,14216,14220,926,14224,14228,14233,14237,14242,14246,2878,14250,14254,2875,14258,14262,14266,2861,14270,14274,14279,14283,14287,5159,14291,14295,14300,14304,14309,6491,14314,14319,14325,14331,14336,14342,14347,6483,14352,14357,6509,14362,14367,6505,14372,14377,6487,14382,14387,14393,14398,14403,14408,14412,14417,9839,14423,14428,14433,14437,14442,14448,14453,14459,14464,14469,14473,14478,14484,14489,14494,14499,14504,14508,14513,14517,14522,14528,14533,14538,14543,14547,14552,14558,14563,14567,2678,14571,14575,14580,14584,14589,14593,826,14597,14601,14606,14610,14615,14619,1325,14623,14627,2689,14631,14635,14639,2675,14643,14647,14652,14656,14660,14664,14667,14671,14676,14680,14685,6435,14690,14695,14701,14706,14712,14717,992,14722,14727,14732,4430,14737,14742,6449,14747,14752,14757,6431,14762,9853,14767,14772,14776,14781,14787,14792,14797,1464,14802,14807,14813,14818,14824,14829,3166,14834,14839,14844,3162,14849,14854,9858,3144,14859,14864,14870,14875,14880,14885,14889,14894,14900,14905,14910,812,14915,14920,14926,14932,14937,14943,14948,3342,14953,14958,14963,3338,14968,14973,3320,14978,14983,14989,14994,14999,15004,15008,15013,15019,15024,822,15029,15035,15040,15045,10693,15050,15055,15060,15065,15069,15074,15079,15084,15088,15093,15099,15104,15109,15114,15118,15123,15129,15134,15139,4604,15144,15149,15154,15158,15163,15168,15173,15177,15182,15187,15192,15196,15201,15207,15212,15217,15222,15226,15231,15237,15242,15247,988,15252,15257,15263,15268,15274,15279,327,15284,15289,15294,2305,15299,15304,15309,2696,15314,15319,15325,15330,15335,15340,15344,15349,15355,15360,15364,819,15368,15372,15377,15381,15386,15390,2668,15394,15398,2665,15402,15406,15410,2651,15414,15418,15423,15427,15431,4486,15435,15439,15444,15448,15452,3208,15456,15460,15465,15470,15474,15479,15484,15489,15493,15498,15502,3222,15506,15510,15514,3205,15518,15522,15527,9873,15531,5156,15535,15539,15544,15548,15552,2712,15556,15560,15565,9877,15570,15575,15580,3499,15585,15590,15594,2726,15598,15602,15606,2709,15610,15614,15619,15623,15627,15631,15634,15638,15643,15647,15652,4282,15657,15662,15668,15674,15679,15685,15691,15696,15702,15707,4269,15712,15717,15722,4265,15727,15732,15738,15743,15748,15753,15757,15762,15767,10019,15772,15777,15783,15789,15794,15800,15806,15811,15816,10268,15821,15826,10211,9881,15831,15837,15842,15847,15852,15856,15861,15867,15872,15877,2968,15882,15887,15893,15899,15904,15910,9886,15916,15922,15927,2986,15932,15937,15942,2964,15947,15952,15956,2891,15960,15964,15969,9900,15974,15979,15984,15988,15992,2905,15996,16000,16004,16008,16011,16015,16020,16024,16028,347,16032,16036,16041,16046,16050,16055,16060,16065,16069,16074,16078,1318,16082,16086,16090,670,16094,16098,16103,16107,16111,1124,16115,16119,16124,0,0,0,15590,2547,13443,15882,14291,6435,13615,13418,2599,13771,13332,11894,12267,1318,11747,13844,12590,915,0,0,12768,11659,13594,15932,2840,11563,15717,14120,11763,12149,16024,12459,15340,12438,1044,3222,0,15753,13398,15674,3114,3183,15325,13234,5159,14237,14193,13869,15242,0,15284,3338,14319,0,3009,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2995,16128,0,0,16132,0,0,0,16136,0,0,0,0,0,0,0,16140,16144,0,16148,0,16152,0,0,0,0,0,0,16157,0,0,0,16162,0,0,0,16168,0,0,0,0,0,0,0,0,0,0,0,0,16174,0,0,0,0,0,0,3030,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16181,0,0,0,0,0,0,3016,16187,0,0,16192,0,0,0,16197,0,0,0,0,0,0,0,16202,16207,0,16212,0,16217,0,0,0,0,0,0,6584,0,0,0,16223,0,0,0,16228,0,0,0,0,0,0,0,16233,0,0,0,0,0,0,0,0,0,0,0,16238,16242,0,0,16247,0,0,0,16252,0,0,0,0,0,0,0,16257,16262,0,0,0,16267,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2998,16273,0,0,16277,0,0,0,16281,0,0,0,0,0,0,0,16285,16289,0,16293,0,16297,0,0,0,0,0,0,806,16302,0,0,1655,0,0,0,16306,16310,0,0,0,0,0,0,16315,16319,0,16323,16327,16332,0,0,0,0,0,0,16337,16341,0,0,16346,0,0,0,16351,0,0,0,0,0,0,0,16356,16361,0,16366,16371,16377,0,0,0,0,0,0,4073,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16383,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16389,16393,0,0,16398,0,0,0,16403,0,16408,0,0,0,0,0,16414,16419,0,16424,16429,16435,0,0,0,0,0,0,2851,16441,0,0,16445,0,0,0,16449,0,0,0,0,0,0,0,16453,16457,0,16461,0,16465,0,0,0,0,0,0,16470,0,0,0,16475,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16481,0,0,0,0,0,0,0,4061,0,0,0,16488,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2854,16493,0,0,16497,0,0,0,16501,0,0,0,0,0,0,0,16505,16509,0,16513,0,16517,0,0,0,0,4993,0,2857,0,0,0,16522,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16527,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16532,0,0,0,16536,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16541,0,16546,0,0,0,0,0,0,4069,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2840,16552,0,0,16556,0,0,0,16560,0,0,0,0,0,0,0,10627,16564,0,16568,0,16572,0,0,0,0,0,0,16577,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16582,0,0,0,0,0,0,0,3613,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3617,16589,0,0,16594,0,0,0,16599,0,0,0,0,0,0,0,16604,16609,0,0,0,16614,0,0,0,0,0,0,16620,0,0,0,16624,0,0,0,16629,0,0,0,0,0,0,0,16634,0,0,0,0,16639,0,0,0,0,0,0,16645,16649,0,0,16654,0,0,16659,16664,0,16669,0,0,0,0,0,16675,16680,0,16685,0,0,0,0,0,0,0,0,4065,0,0,0,16690,0,0,0,16695,0,0,0,0,0,0,0,16700,16705,0,0,0,0,0,0,0,0,0,0,324,16710,0,0,16714,0,0,0,16718,0,0,0,0,0,0,0,16722,16726,0,16730,0,16734,0,0,0,0,0,0,803,16739,16743,0,16748,0,0,0,16752,0,16756,0,0,0,0,0,16761,16765,0,16769,16773,16778,0,0,0,11713,0,0,16783,16787,0,0,16792,0,0,0,16797,0,0,0,0,0,0,0,16802,16807,0,16812,16817,16823,0,0,0,0,0,0,16829,16833,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16838,16842,0,0,16847,0,0,0,16852,0,0,0,0,0,0,0,16857,16862,0,16867,16872,16878,0,0,0,0,0,0,3468,16884,0,0,16888,0,0,0,16892,0,0,0,0,0,0,0,16896,16900,0,16904,0,16908,0,0,0,0,0,0,16913,0,0,0,16918,0,0,0,16924,0,0,0,0,0,0,0,16930,16936,0,0,16942,16949,0,0,0,0,0,0,16956,0,0,0,0,0,0,0,16960,0,0,0,0,0,0,0,0,16965,0,16970,0,0,0,0,0,0,0,0,3471,16975,0,0,16979,0,0,0,16983,0,0,0,0,0,0,0,16987,16991,0,16995,0,16999,0,0,0,0,0,0,2779,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17004,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17010,0,0,0,17014,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17019,0,0,0,17023,0,0,0,17028,0,0,0,0,0,0,0,0,17033,0,17038,0,0,0,0,0,0,0,0,3454,17043,0,0,17047,0,0,17051,17055,0,17059,0,0,0,0,0,17064,17068,0,17072,0,17076,0,0,0,0,0,0,17081,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17086,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3577,0,0,0,17093,0,0,0,17098,0,0,0,0,0,0,0,17103,0,0,17108,0,0,0,0,0,0,0,0,17113,0,0,0,17117,0,0,0,17122,0,0,0,0,0,0,0,17127,0,0,17132,0,17137,0,0,0,0,0,0,17143,0,0,0,17147,0,0,0,17152,0,0,0,0,0,0,0,17157,17162,0,17167,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3457,17172,0,0,17176,0,0,0,17180,0,0,0,0,0,0,0,17184,17188,0,17192,0,17196,0,0,0,0,0,0,429,17201,0,0,10033,0,0,0,17205,0,0,0,0,17209,0,0,17214,17218,0,17222,0,17226,0,0,0,0,0,0,1616,17231,0,0,17236,0,0,0,17241,0,0,0,0,0,0,0,17246,17251,0,17256,17261,17267,0,0,0,0,0,0,17273,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17277,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17283,17287,0,0,17292,0,0,0,17297,0,17302,0,0,0,0,0,17308,17313,0,17318,0,17323,0,0,0,0,0,0,1620,17329,0,0,10282,0,0,0,17333,0,0,0,0,0,0,0,17337,17341,0,17345,0,10541,0,0,0,0,0,0,17349,17354,0,0,17360,0,0,0,17366,0,0,0,0,0,0,0,17372,17378,0,17384,17390,17397,0,0,0,0,0,0,17404,0,0,0,17408,0,0,0,17413,0,0,0,0,0,0,0,0,17418,0,0,0,0,0,0,0,0,0,0,2537,17423,0,0,9220,0,0,0,17427,0,0,0,0,17431,0,0,17436,17440,0,17444,0,17448,0,0,0,14006,0,0,4211,17453,0,0,17458,0,0,0,17463,0,0,0,0,0,0,0,0,0,0,17468,0,17473,0,0,0,0,0,0,17479,17484,0,0,17490,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17496,0,17502,0,0,0,0,0,0,1631,17509,0,0,17514,0,0,0,17519,0,0,0,0,0,0,0,0,17524,0,17529,0,17534,0,0,0,0,0,0,17540,0,0,0,17544,0,0,0,17549,0,0,0,0,0,0,0,0,17554,0,17559,0,0,0,0,0,0,0,0,2523,17564,0,0,10169,0,0,0,17568,0,0,0,0,17572,0,0,17577,0,0,17581,0,17585,0,0,0,0,0,0,17590,0,0,0,17595,0,0,0,17601,0,0,0,0,0,0,0,17607,0,0,0,0,17613,0,0,0,0,0,0,4198,17620,0,0,915,17625,17629,17634,1639,17639,17644,17649,17653,17657,17662,17667,17672,17677,17682,17687,17692,17696,17700,17705,17709,17714,17719,17723,17727,13367,13375,17731,17735,17739,17744,17750,17756,17761,17767,17773,17778,17783,17789,17795,17801,17807,17813,17819,17825,17830,17835,17841,17846,17852,17858,17863,17868,17873,17878,17883,6640,17888,17893,17899,17905,17910,17916,17922,17927,17932,17938,17944,17950,17956,17962,17968,17974,17979,17984,17990,17995,18001,18007,18012,18017,18022,18027,18032,18037,18042,18048,18055,18062,18068,18075,18082,18088,18094,18101,18108,18115,18122,18129,18136,18143,18149,18155,18162,18168,18175,18182,18188,18194,18200,18206,18212,1140,18218,18223,18229,18235,18240,18246,18252,18257,18262,18268,18274,18280,18286,18292,18298,18304,18309,18314,18320,18325,18331,18337,18342,18347,18352,18357,18362,3239,18367,18371,18376,10345,18381,18386,18391,18395,18399,18404,18409,18414,18419,18424,18429,18434,18438,18442,18447,18451,18456,18461,18465,18469,13410,13418,18473,18477,18482,18488,18495,18502,18508,18515,18522,18528,18534,18541,18548,18555,18562,18569,18576,18583,18589,18595,18602,18608,18615,18622,18628,18634,18640,18646,18652,6662,18658,18663,18669,18675,18680,18686,18692,18697,18702,18708,18714,18720,18726,18732,18738,18744,18749,18754,18760,18765,18771,18777,18782,18787,18792,18797,18802,3242,18807,18811,18816,18821,18825,18830,18835,18839,18843,18848,18853,18858,18863,18868,18873,18878,18882,18886,18891,18895,18900,18905,18909,18913,13398,13406,18917,3245,18921,18926,18932,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,6,24,54,24,54,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18938,18943,18949,18955,18960,18965,18971,18977,18983,18989,18995,19001,19007,19012,19017,19023,19028,19034,19040,19045,19050,19055,19060,19065,19070,19075,19081,19088,19095,19101,19108,19115,19121,19127,19134,19141,19148,19155,19162,19169,19176,19182,19188,19195,19201,19208,19215,19221,19227,19233,19239,19245,19251,19255,19260,19266,19272,19277,19283,19289,19294,19299,19305,19311,19317,19323,19329,19335,19341,19346,19351,19357,19362,19368,19374,19379,19384,19389,19394,19399,6666,19404,19409,19415,10825,19421,19427,19433,19438,19443,19449,19455,19461,19467,19473,19479,19485,19490,19495,19501,19506,19512,19518,19523,19528,19533,19538,19543,3225,19548,19552,19557,19562,19566,19571,19576,19580,19584,19589,19594,19599,19604,19609,19614,19619,19623,19627,19632,19636,19641,19646,19650,19654,13422,13430,19658,19662,19667,19673,19680,19687,19693,19700,19707,19713,19719,19726,19733,19740,19747,19754,19761,19768,19774,19780,19787,19793,19800,19807,19813,19819,19825,19831,19837,3263,19843,19848,19854,19860,19865,19871,19877,19882,19887,19893,19899,19905,19911,19917,19923,19929,19934,19939,19945,19950,19956,19962,19967,19972,19977,19982,19987,3249,19992,19997,20003,20009,20014,20020,20026,20031,20036,20042,20048,20054,20060,20066,20072,20078,20083,20088,20094,20099,20105,20111,20116,20121,20126,20131,20136,6644,20141,20146,20152,20158,20163,20169,20175,20180,20185,20191,20197,20203,20209,20215,20221,20227,20232,20237,20243,20248,20254,20260,20265,20270,20275,20280,20285,20290,20294,20299,20305,20311,20316,20322,20328,20333,20338,20344,20350,20356,20362,20368,20374,20380,20385,20390,20396,7478,8154,20401,20406,20411,20416,20421,20426,6648,20431,20436,20442,20448,20453,20459,20465,20470,20475,20481,20487,20493,20499,20505,20511,20517,20522,20527,20533,20538,20544,20550,20555,20560,20565,20570,20575,3228,20580,20584,20589,20594,20598,20603,20608,20612,20616,20621,20626,20631,20636,20641,20646,20651,20655,20659,20664,20668,20673,20678,20682,20686,13336,13344,20690,1060,20694,20699,20705,20711,20716,20722,20728,20733,20738,20744,20750,20756,20762,20768,20774,20780,20785,20790,20796,20801,20807,20813,20818,20823,13567,13577,20828,20833,20838,20844,20851,20858,20864,20871,20878,20884,20890,20897,20904,20911,20918,20925,20932,20939,20945,20951,20958,20964,20971,20978,20984,20990,20996,21002,21008,21014,21019,21025,21032,21039,21045,21052,21059,21065,21071,21078,21085,21092,21099,21106,21113,21120,21126,21132,21139,21145,21152,21159,21165,21171,21177,21183,21189,21195,21201,21208,21216,21224,21231,21239,21247,21254,21261,21269,21277,21285,21293,21301,21309,21317,21324,21331,21339,21346,21354,21362,21369,21376,21383,21390,21397,21404,21409,21415,21422,21429,21435,21442,21449,21455,21461,21468,21475,21482,21489,21496,21503,21510,21516,21522,21529,21535,21542,21549,21555,21561,21567,21573,21579,3285,21585,21590,21596,21602,21607,21613,21619,21624,21629,21635,21641,21647,21653,21659,21665,21671,21676,21681,21687,21692,21698,21704,21709,21714,13620,13630,21719,21724,21730,21737,21745,21753,21760,21768,21776,21783,21790,21798,21806,21814,21822,21830,21838,21846,21853,21860,21868,21875,21883,21891,21898,21905,21912,21919,21926,21933,21938,21944,21951,21958,21964,21971,21978,21984,21990,21997,22004,22011,22018,22025,22032,22039,22045,22051,22058,22064,22071,22078,22084,22090,22096,22102,22108,3289,22114,22119,22125,22131,22136,22142,22148,22153,22158,22164,22170,22176,22182,22188,22194,22200,22205,22210,22216,22221,22227,22233,22238,22243,13605,13615,22248,6410,22253,22259,22266,22273,22279,22286,22293,22299,22305,22312,22319,22326,22333,22340,22347,22354,22360,22366,22373,22379,22386,22393,22399,22405,22411,22417,22423,22429,22435,22442,22450,22458,22465,22473,22481,22488,22495,22503,22511,22519,22527,22535,22543,22551,22558,22565,22573,22580,22588,22596,22603,22610,22617,22624,22631,22638,22643,22649,22656,22663,22669,22676,22683,22689,22695,22702,22709,22716,22723,22730,22737,22744,22750,22756,22763,22769,22776,22783,22789,22795,22801,22807,22813,22819,22824,22830,22837,22844,22850,22857,22864,22870,22876,22883,22890,22897,22904,22911,22918,22925,22931,22937,22944,22950,22957,22964,22970,22976,22982,22988,22994,3267,23000,23005,23011,23017,23022,23028,23034,23039,23044,23050,23056,23062,23068,23074,23080,23086,23091,23096,23102,23107,23113,23119,23124,23129,13635,13645,23134,23139,23145,23152,23160,23168,23175,23183,23191,23198,23205,23213,23221,23229,23237,23245,23253,23261,23268,23275,23283,23290,23298,23306,23313,23320,23327,23334,23341,6426,23348,23354,23361,23368,23374,23381,23388,23394,23400,23407,23414,23421,23428,23435,23442,23449,23455,23461,23468,23474,23481,23488,23494,23500,23506,23512,23518,6415,23524,23530,23537,23544,23550,23557,23564,23570,23576,23583,23590,23597,23604,23611,23618,23625,23631,23637,23644,23650,23657,23664,23670,23676,23682,23688,23694,23700,23705,23711,23718,23725,23731,23738,23745,23751,23757,23764,23771,23778,23785,23792,23799,23806,23812,23818,23825,23831,23838,23845,23851,23857,23863,23869,23875,23881,23886,23892,23899,23906,23912,23919,23926,23932,23938,23945,23952,23959,23966,23973,23980,23987,23993,23999,24006,24012,24019,24026,24032,24038,24044,24050,24056,24062,24067,24073,24080,24087,24093,24100,24107,24113,24119,24126,24133,24140,24147,24154,24161,24168,24174,24180,24187,24193,24200,24207,24213,24219,24225,24231,24237,3271,24243,24248,24254,24260,24265,24271,24277,24282,24287,24293,24299,24305,24311,24317,24323,24329,24334,24339,24345,24350,24356,24362,24367,24372,13540,24377,24382,842,24387,24391,24396,24401,24405,24410,24415,24419,24423,24428,24433,24438,24443,24448,24453,24458,24462,24466,24471,24475,24480,24485,24489,24493,24497,12997,3488,24501,24505,24510,24516,24522,24527,24533,24539,24544,24549,24555,24561,24567,24573,24579,24585,24591,24596,24601,24607,24612,24618,24624,24629,24634,24639,24644,24649,848,24654,24659,24665,10893,24671,24677,24683,24688,24693,24699,24705,24711,24717,24723,24729,24735,24740,24745,24751,24756,24762,24768,24773,24778,24783,24788,24793,24798,24803,24809,24816,24823,24829,24836,24843,24849,24855,24862,24869,24876,24883,24890,24897,24904,24910,24916,24923,24929,24936,24943,24949,24955,24961,24967,24973,24979,24983,24988,24994,25000,25005,25011,25017,25022,25027,25033,25039,25045,25051,25057,25063,25069,25074,25079,25085,25090,25096,25102,25107,25112,25117,25122,25127,2957,25132,25136,25141,25146,25150,25155,25160,25164,25168,25173,25178,25183,25188,25193,25198,25203,25207,25211,25216,25220,25225,25230,25234,25238,25242,13031,25246,25250,25255,25261,25268,25275,25281,25288,25295,25301,25307,25314,25321,25328,25335,25342,25349,25356,25362,25368,25375,25381,25388,25395,25401,25407,25413,25419,25425,2982,25431,25436,25442,25448,25453,25459,25465,25470,25475,25481,25487,25493,25499,25505,25511,25517,25522,25527,25533,25538,25544,25550,25555,25560,25565,25570,25575,1400,25580,25584,25589,25594,25598,25603,25608,25612,25616,25621,25626,25631,25636,25641,25646,25651,25655,25659,25664,25668,25673,25678,25682,25686,13015,13023,25690,2960,25694,25699,25705,25711,25716,25722,25728,25733,25738,25744,25750,25756,25762,25768,25774,25780,25785,25790,25796,25801,25807,25813,25818,25823,25828,25833,25838,25843,25848,25854,25861,25868,25874,25881,25888,25894,25900,25907,25914,25921,25928,25935,25942,25949,25955,25961,25968,25974,25981,25988,25994,26000,26006,26012,26018,26024,26028,26033,26039,26045,26050,26056,26062,26067,26072,26078,26084,26090,26096,26102,26108,26114,26119,26124,26130,26135,26141,26147,26152,26157,26162,26167,26172,2986,26177,26182,26188,26194,26199,26205,26211,26216,26221,26227,26233,26239,26245,26251,26257,26263,26268,26273,26279,26284,26290,26296,26301,26306,15922,15932,26311,2947,26316,26320,26325,26330,26334,26339,26344,26348,26352,26357,26362,26367,26372,26377,26382,10623,26387,26391,26396,26400,26405,26410,26414,26418,13035,13043,26422,26426,26431,26437,26444,26451,26457,26464,26471,26477,26483,26490,26497,26504,26511,26518,26525,26532,26538,26544,26551,26557,26564,26571,26577,26583,26589,26595,26601,3791,26607,26612,26618,26624,26629,26635,26641,26646,26651,26657,26663,26669,26675,26681,26687,26693,26698,26703,26709,26714,26720,26726,26731,26736,26741,26746,26751,26756,26760,26765,26771,26777,26782,26788,26794,26799,26804,26810,26816,26822,26828,26834,26840,26846,26851,26856,26862,26867,26873,26879,26884,26889,26894,26899,26904,2964,26909,26914,26920,26926,26931,26937,26943,26948,26953,26959,26965,26971,26977,26983,26989,26995,27000,27005,27011,27016,27022,27028,27033,27038,15937,15947,27043,27048,27052,27057,27063,27069,27074,27080,27086,27091,27096,27102,27108,27114,27120,27126,27132,27138,27143,27148,27154,27159,27165,27171,27176,27181,27186,27191,27196,2968,27201,27206,27212,27218,27223,27229,27235,27240,27245,27251,27257,27263,27269,27275,27281,27287,27292,27297,27303,27308,27314,27320,27325,27330,15872,15882,27335,2950,27340,27344,27349,27354,27358,27363,27368,27372,27376,27381,27386,27391,27396,27401,27406,27411,27415,27419,27424,27428,27433,27438,27442,27446,12967,12975,27450,816,27454,27458,27463,27468,27472,27477,27482,681,27486,27491,27496,27501,27506,27511,27516,27521,10919,27525,27530,27534,27539,27544,27548,27552,12510,12518,27556,27560,27564,27569,27575,27581,27586,27592,27598,27603,27608,27614,27620,27626,27632,27638,27644,27650,27655,27660,27666,27671,27677,27683,27688,27693,27698,27703,27708,27713,27717,27722,27728,27734,27739,27745,27751,27756,27761,27767,27773,27779,27785,27791,27797,27803,27808,27813,27819,27824,27830,27836,27841,27846,27851,27856,27861,27866,27871,27877,27884,27891,27897,27904,27911,27917,27923,27930,27937,27944,27951,27958,27965,27972,27978,27984,27991,27997,28004,28011,28017,28023,28029,28035,28041,28047,28051,28056,28062,28068,28073,28079,28085,28090,28095,28101,28107,28113,28119,28125,28131,28137,28142,28147,28153,28158,28164,28170,28175,28180,28185,28190,28195,432,82,28200,28205,28210,28214,28219,28224,28228,28232,28237,28242,28247,28252,28257,28262,28267,28271,28275,28280,28284,28289,28294,28298,28302,28306,12543,28310,28314,28319,28325,28332,28339,28345,28352,28359,28365,28371,28378,28385,28392,28399,28406,28413,28420,28426,28432,28439,28445,28452,28459,28465,28471,28477,28483,28489,28495,28499,28504,28510,28516,28521,28527,28533,28538,28543,28549,28555,28561,28567,28573,28579,28585,28590,28595,28601,28606,28612,28618,28623,28628,28633,28638,28643,3194,28648,28652,28657,1643,28662,28667,28672,28676,28680,28685,28690,28695,28700,28705,28710,28715,28719,28723,28728,28732,28737,28742,28746,28750,618,12535,28754,3197,28758,28763,28769,28775,28780,28786,28792,28797,28802,28808,28814,28820,28826,28832,28838,28844,28849,28854,28860,28865,28871,28877,28882,28887,28892,28897,28902,28907,28912,28918,28925} diff --git a/packaging/nominatim-api/extra_src/paths.py b/packaging/nominatim-api/extra_src/paths.py index 6131319c..797acbb5 100644 --- a/packaging/nominatim-api/extra_src/paths.py +++ b/packaging/nominatim-api/extra_src/paths.py @@ -9,7 +9,6 @@ Path settings for extra data used by Nominatim. """ from pathlib import Path -PHPLIB_DIR = None DATA_DIR = None SQLLIB_DIR = None CONFIG_DIR = (Path(__file__) / '..' / 'resources' / 'settings').resolve() diff --git a/packaging/nominatim-db/extra_src/nominatim_db/paths.py b/packaging/nominatim-db/extra_src/nominatim_db/paths.py index 2294834f..796ff08b 100644 --- a/packaging/nominatim-db/extra_src/nominatim_db/paths.py +++ b/packaging/nominatim-db/extra_src/nominatim_db/paths.py @@ -9,7 +9,6 @@ Path settings for extra data used by Nominatim. """ from pathlib import Path -PHPLIB_DIR = None DATA_DIR = (Path(__file__) / '..' / 'resources').resolve() SQLLIB_DIR = (DATA_DIR / 'lib-sql') CONFIG_DIR = (DATA_DIR / 'settings') diff --git a/packaging/nominatim-db/scripts/nominatim b/packaging/nominatim-db/scripts/nominatim index 39e703dc..184ab4c6 100755 --- a/packaging/nominatim-db/scripts/nominatim +++ b/packaging/nominatim-db/scripts/nominatim @@ -2,4 +2,4 @@ from nominatim_db import cli -exit(cli.nominatim(module_dir=None, osm2pgsql_path=None)) +exit(cli.nominatim(osm2pgsql_path=None)) diff --git a/phpcs.xml b/phpcs.xml deleted file mode 100644 index ab9d3969..00000000 --- a/phpcs.xml +++ /dev/null @@ -1,172 +0,0 @@ - - - - Nominatim coding standard - - - - - - - ./lib/template/*html* - ./lib/template/includes/ - ./module/ - ./website/css - ./website/js - - - - - - - - - - - - 0 - - - - - - - - - - - - - - - 0 - - - - - - - - - - 0 - - - - - 0 - - - - 0 - - - - 0 - - - - - 0 - - - - 0 - - - - 0 - - - - - - - - - - - - 0 - - - - - - - - - - - - 0 - - - - - 0 - - - 0 - - - - - 0 - - - - - - - - - - - - - - - - 0 - - - - - - - - - - - 0 - - - - - 0 - - - - 0 - - - - - diff --git a/settings/env.defaults b/settings/env.defaults index f4c33e77..b8c66667 100644 --- a/settings/env.defaults +++ b/settings/env.defaults @@ -12,24 +12,12 @@ NOMINATIM_DATABASE_DSN="pgsql:dbname=nominatim" # Nominatim sets up read-only access for this user during installation. NOMINATIM_DATABASE_WEBUSER="www-data" -# Directory where to find the PostgreSQL server module. -# When empty the module is expected to be located in the 'module' subdirectory -# in the project directory. -# Changing this value requires to run 'nominatim refresh --functions'. -NOMINATIM_DATABASE_MODULE_PATH= - # Tokenizer used for normalizing and parsing queries and names. # The tokenizer is set up during import and cannot be changed afterwards # without a reimport. # Currently available tokenizers: icu, legacy NOMINATIM_TOKENIZER="icu" -# Number of occurrences of a word before it is considered frequent. -# Similar to the concept of stop words. Frequent partial words get ignored -# or handled differently during search. -# Changing this value requires a reimport. -NOMINATIM_MAX_WORD_FREQUENCY=50000 - # If true, admin level changes on places with many contained children are blocked. NOMINATIM_LIMIT_REINDEXING=yes @@ -40,12 +28,6 @@ NOMINATIM_LIMIT_REINDEXING=yes # Currently only affects the initial import of country names and special phrases. NOMINATIM_LANGUAGES= -# Rules for normalizing terms for comparisons. -# The default is to remove accents and punctuation and to lower-case the -# term. Spaces are kept but collapsed to one standard space. -# Changing this value requires a reimport. -NOMINATIM_TERM_NORMALIZATION=":: NFD (); [[:Nonspacing Mark:] [:Cf:]] >; :: lower (); [[:Punctuation:][:Space:]]+ > ' '; :: NFC ();" - # Configuration file for the tokenizer. # The content depends on the tokenizer used. If left empty the default settings # for the chosen tokenizer will be used. The configuration can only be set @@ -177,16 +159,6 @@ NOMINATIM_MAPICON_URL= # When unset, the local language (i.e. the name tag without suffix) will be used. NOMINATIM_DEFAULT_LANGUAGE= -# Enable a special batch query mode. -# This feature is currently undocumented and potentially broken. -NOMINATIM_SEARCH_BATCH_MODE=no - -# Threshold for searches by name only. -# Threshold where the lookup strategy in the database is switched. If there -# are less occurrences of a tem than given, the search does the lookup only -# against the name, otherwise it uses indexes for name and address. -NOMINATIM_SEARCH_NAME_ONLY_THRESHOLD=500 - # Maximum number of OSM ids accepted by /lookup. NOMINATIM_LOOKUP_MAX_COUNT=50 diff --git a/src/nominatim_api/core.py b/src/nominatim_api/core.py index ac579862..ff0db39f 100644 --- a/src/nominatim_api/core.py +++ b/src/nominatim_api/core.py @@ -7,7 +7,8 @@ """ Implementation of classes for API access via libraries. """ -from typing import Mapping, Optional, Any, AsyncIterator, Dict, Sequence, List, Tuple, cast +from typing import Mapping, Optional, Any, AsyncIterator, Dict, Sequence, List,\ + Union, Tuple, cast import asyncio import sys import contextlib @@ -41,7 +42,7 @@ class NominatimAPIAsync: #pylint: disable=too-many-instance-attributes This class should usually be used as a context manager in 'with' context. """ - def __init__(self, project_dir: Path, + def __init__(self, project_dir: Optional[Union[str, Path]] = None, environ: Optional[Mapping[str, str]] = None, loop: Optional[asyncio.AbstractEventLoop] = None) -> None: """ Initiate a new frontend object with synchronous API functions. @@ -365,7 +366,7 @@ class NominatimAPI: This class should usually be used as a context manager in 'with' context. """ - def __init__(self, project_dir: Path, + def __init__(self, project_dir: Optional[Union[str, Path]] = None, environ: Optional[Mapping[str, str]] = None) -> None: """ Initiate a new frontend object with synchronous API functions. diff --git a/src/nominatim_api/search/db_search_builder.py b/src/nominatim_api/search/db_search_builder.py index 0269cf1f..b4346ee6 100644 --- a/src/nominatim_api/search/db_search_builder.py +++ b/src/nominatim_api/search/db_search_builder.py @@ -167,8 +167,7 @@ class SearchBuilder: expected_count = sum(t.count for t in hnrs) partials = {t.token: t.addr_count for trange in address - for t in self.query.get_partials_list(trange) - if t.is_indexed} + for t in self.query.get_partials_list(trange)} if not partials: # can happen when none of the partials is indexed @@ -219,11 +218,9 @@ class SearchBuilder: addr_partials = [t for r in address for t in self.query.get_partials_list(r)] addr_tokens = list({t.token for t in addr_partials}) - partials_indexed = all(t.is_indexed for t in name_partials.values()) \ - and all(t.is_indexed for t in addr_partials) exp_count = min(t.count for t in name_partials.values()) / (2**(len(name_partials) - 1)) - if (len(name_partials) > 3 or exp_count < 8000) and partials_indexed: + if (len(name_partials) > 3 or exp_count < 8000): yield penalty, exp_count, dbf.lookup_by_names(list(name_partials.keys()), addr_tokens) return @@ -232,8 +229,6 @@ class SearchBuilder: name_fulls = self.query.get_tokens(name, TokenType.WORD) if name_fulls: fulls_count = sum(t.count for t in name_fulls) - if partials_indexed: - penalty += 1.2 * sum(t.penalty for t in addr_partials if not t.is_indexed) if fulls_count < 80000 or addr_count < 50000: yield penalty,fulls_count / (2**len(addr_tokens)), \ @@ -243,8 +238,7 @@ class SearchBuilder: # To catch remaining results, lookup by name and address # We only do this if there is a reasonable number of results expected. exp_count = exp_count / (2**len(addr_tokens)) if addr_tokens else exp_count - if exp_count < 10000 and addr_count < 20000\ - and all(t.is_indexed for t in name_partials.values()): + if exp_count < 10000 and addr_count < 20000: penalty += 0.35 * max(1 if name_fulls else 0.1, 5 - len(name_partials) - len(addr_tokens)) yield penalty, exp_count,\ @@ -260,11 +254,10 @@ class SearchBuilder: addr_restrict_tokens = [] addr_lookup_tokens = [] for t in addr_partials: - if t.is_indexed: - if t.addr_count > 20000: - addr_restrict_tokens.append(t.token) - else: - addr_lookup_tokens.append(t.token) + if t.addr_count > 20000: + addr_restrict_tokens.append(t.token) + else: + addr_lookup_tokens.append(t.token) if addr_restrict_tokens: lookup.append(dbf.FieldLookup('nameaddress_vector', @@ -289,7 +282,7 @@ class SearchBuilder: addr_restrict_tokens = [] addr_lookup_tokens = [t.token for t in addr_partials if t.is_indexed] else: - addr_restrict_tokens = [t.token for t in addr_partials if t.is_indexed] + addr_restrict_tokens = [t.token for t in addr_partials] addr_lookup_tokens = [] return dbf.lookup_by_any_name([t.token for t in name_fulls], diff --git a/src/nominatim_api/search/icu_tokenizer.py b/src/nominatim_api/search/icu_tokenizer.py index 7bd2b092..c2a26510 100644 --- a/src/nominatim_api/search/icu_tokenizer.py +++ b/src/nominatim_api/search/icu_tokenizer.py @@ -123,7 +123,7 @@ class ICUToken(qmod.Token): lookup_word = row.word_token return ICUToken(penalty=penalty, token=row.word_id, count=max(1, count), - lookup_word=lookup_word, is_indexed=True, + lookup_word=lookup_word, word_token=row.word_token, info=row.info, addr_count=max(1, addr_count)) @@ -264,7 +264,9 @@ class ICUQueryAnalyzer(AbstractQueryAnalyzer): if len(part.token) <= 4 and part[0].isdigit()\ and not node.has_tokens(i+1, qmod.TokenType.HOUSENUMBER): query.add_token(qmod.TokenRange(i, i+1), qmod.TokenType.HOUSENUMBER, - ICUToken(0.5, 0, 1, 1, part.token, True, part.token, None)) + ICUToken(penalty=0.5, token=0, + count=1, addr_count=1, lookup_word=part.token, + word_token=part.token, info=None)) def rerank_tokens(self, query: qmod.QueryStruct, parts: QueryParts) -> None: diff --git a/src/nominatim_api/search/legacy_tokenizer.py b/src/nominatim_api/search/legacy_tokenizer.py deleted file mode 100644 index c7b10119..00000000 --- a/src/nominatim_api/search/legacy_tokenizer.py +++ /dev/null @@ -1,273 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# -# This file is part of Nominatim. (https://nominatim.org) -# -# Copyright (C) 2024 by the Nominatim developer community. -# For a full list of authors see the git log. -""" -Implementation of query analysis for the legacy tokenizer. -""" -from typing import Tuple, Dict, List, Optional, Iterator, Any, cast -from copy import copy -from collections import defaultdict -import dataclasses - -import sqlalchemy as sa - -from ..typing import SaRow -from ..connection import SearchConnection -from ..logging import log -from . import query as qmod -from .query_analyzer_factory import AbstractQueryAnalyzer - -def yield_words(terms: List[str], start: int) -> Iterator[Tuple[str, qmod.TokenRange]]: - """ Return all combinations of words in the terms list after the - given position. - """ - total = len(terms) - for first in range(start, total): - word = terms[first] - yield word, qmod.TokenRange(first, first + 1) - for last in range(first + 1, min(first + 20, total)): - word = ' '.join((word, terms[last])) - yield word, qmod.TokenRange(first, last + 1) - - -@dataclasses.dataclass -class LegacyToken(qmod.Token): - """ Specialised token for legacy tokenizer. - """ - word_token: str - category: Optional[Tuple[str, str]] - country: Optional[str] - operator: Optional[str] - - @property - def info(self) -> Dict[str, Any]: - """ Dictionary of additional properties of the token. - Should only be used for debugging purposes. - """ - return {'category': self.category, - 'country': self.country, - 'operator': self.operator} - - - def get_category(self) -> Tuple[str, str]: - assert self.category - return self.category - - -class LegacyQueryAnalyzer(AbstractQueryAnalyzer): - """ Converter for query strings into a tokenized query - using the tokens created by a legacy tokenizer. - """ - - def __init__(self, conn: SearchConnection) -> None: - self.conn = conn - - async def setup(self) -> None: - """ Set up static data structures needed for the analysis. - """ - self.max_word_freq = int(await self.conn.get_property('tokenizer_maxwordfreq')) - if 'word' not in self.conn.t.meta.tables: - sa.Table('word', self.conn.t.meta, - sa.Column('word_id', sa.Integer), - sa.Column('word_token', sa.Text, nullable=False), - sa.Column('word', sa.Text), - sa.Column('class', sa.Text), - sa.Column('type', sa.Text), - sa.Column('country_code', sa.Text), - sa.Column('search_name_count', sa.Integer), - sa.Column('operator', sa.Text)) - - - async def analyze_query(self, phrases: List[qmod.Phrase]) -> qmod.QueryStruct: - """ Analyze the given list of phrases and return the - tokenized query. - """ - log().section('Analyze query (using Legacy tokenizer)') - - normalized = [] - if phrases: - for row in await self.conn.execute(sa.select(*(sa.func.make_standard_name(p.text) - for p in phrases))): - normalized = [qmod.Phrase(p.ptype, r) for r, p in zip(row, phrases) if r] - break - - query = qmod.QueryStruct(normalized) - log().var_dump('Normalized query', query.source) - if not query.source: - return query - - parts, words = self.split_query(query) - lookup_words = list(words.keys()) - log().var_dump('Split query', parts) - log().var_dump('Extracted words', lookup_words) - - for row in await self.lookup_in_db(lookup_words): - for trange in words[row.word_token.strip()]: - token, ttype = self.make_token(row) - if ttype == qmod.TokenType.NEAR_ITEM: - if trange.start == 0: - query.add_token(trange, qmod.TokenType.NEAR_ITEM, token) - elif ttype == qmod.TokenType.QUALIFIER: - query.add_token(trange, qmod.TokenType.QUALIFIER, token) - if trange.start == 0 or trange.end == query.num_token_slots(): - token = copy(token) - token.penalty += 0.1 * (query.num_token_slots()) - query.add_token(trange, qmod.TokenType.NEAR_ITEM, token) - elif ttype != qmod.TokenType.PARTIAL or trange.start + 1 == trange.end: - query.add_token(trange, ttype, token) - - self.add_extra_tokens(query, parts) - self.rerank_tokens(query) - - log().table_dump('Word tokens', _dump_word_tokens(query)) - - return query - - - def normalize_text(self, text: str) -> str: - """ Bring the given text into a normalized form. - - This only removes case, so some difference with the normalization - in the phrase remains. - """ - return text.lower() - - - def split_query(self, query: qmod.QueryStruct) -> Tuple[List[str], - Dict[str, List[qmod.TokenRange]]]: - """ Transliterate the phrases and split them into tokens. - - Returns a list of transliterated tokens and a dictionary - of words for lookup together with their position. - """ - parts: List[str] = [] - phrase_start = 0 - words = defaultdict(list) - for phrase in query.source: - query.nodes[-1].ptype = phrase.ptype - for trans in phrase.text.split(' '): - if trans: - for term in trans.split(' '): - if term: - parts.append(trans) - query.add_node(qmod.BreakType.TOKEN, phrase.ptype) - query.nodes[-1].btype = qmod.BreakType.WORD - query.nodes[-1].btype = qmod.BreakType.PHRASE - for word, wrange in yield_words(parts, phrase_start): - words[word].append(wrange) - phrase_start = len(parts) - query.nodes[-1].btype = qmod.BreakType.END - - return parts, words - - - async def lookup_in_db(self, words: List[str]) -> 'sa.Result[Any]': - """ Return the token information from the database for the - given word tokens. - """ - t = self.conn.t.meta.tables['word'] - - sql = t.select().where(t.c.word_token.in_(words + [' ' + w for w in words])) - - return await self.conn.execute(sql) - - - def make_token(self, row: SaRow) -> Tuple[LegacyToken, qmod.TokenType]: - """ Create a LegacyToken from the row of the word table. - Also determines the type of token. - """ - penalty = 0.0 - is_indexed = True - - rowclass = getattr(row, 'class') - - if row.country_code is not None: - ttype = qmod.TokenType.COUNTRY - lookup_word = row.country_code - elif rowclass is not None: - if rowclass == 'place' and row.type == 'house': - ttype = qmod.TokenType.HOUSENUMBER - lookup_word = row.word_token[1:] - elif rowclass == 'place' and row.type == 'postcode': - ttype = qmod.TokenType.POSTCODE - lookup_word = row.word - else: - ttype = qmod.TokenType.NEAR_ITEM if row.operator in ('in', 'near')\ - else qmod.TokenType.QUALIFIER - lookup_word = row.word - elif row.word_token.startswith(' '): - ttype = qmod.TokenType.WORD - lookup_word = row.word or row.word_token[1:] - else: - ttype = qmod.TokenType.PARTIAL - lookup_word = row.word_token - penalty = 0.21 - if row.search_name_count > self.max_word_freq: - is_indexed = False - - return LegacyToken(penalty=penalty, token=row.word_id, - count=max(1, row.search_name_count or 1), - addr_count=1, # not supported - lookup_word=lookup_word, - word_token=row.word_token.strip(), - category=(rowclass, row.type) if rowclass is not None else None, - country=row.country_code, - operator=row.operator, - is_indexed=is_indexed),\ - ttype - - - def add_extra_tokens(self, query: qmod.QueryStruct, parts: List[str]) -> None: - """ Add tokens to query that are not saved in the database. - """ - for part, node, i in zip(parts, query.nodes, range(1000)): - if len(part) <= 4 and part.isdigit()\ - and not node.has_tokens(i+1, qmod.TokenType.HOUSENUMBER): - query.add_token(qmod.TokenRange(i, i+1), qmod.TokenType.HOUSENUMBER, - LegacyToken(penalty=0.5, token=0, count=1, addr_count=1, - lookup_word=part, word_token=part, - category=None, country=None, - operator=None, is_indexed=True)) - - - def rerank_tokens(self, query: qmod.QueryStruct) -> None: - """ Add penalties to tokens that depend on presence of other token. - """ - for _, node, tlist in query.iter_token_lists(): - if tlist.ttype == qmod.TokenType.POSTCODE: - for repl in node.starting: - if repl.end == tlist.end and repl.ttype != qmod.TokenType.POSTCODE \ - and (repl.ttype != qmod.TokenType.HOUSENUMBER - or len(tlist.tokens[0].lookup_word) > 4): - repl.add_penalty(0.39) - elif tlist.ttype == qmod.TokenType.HOUSENUMBER \ - and len(tlist.tokens[0].lookup_word) <= 3: - if any(c.isdigit() for c in tlist.tokens[0].lookup_word): - for repl in node.starting: - if repl.end == tlist.end and repl.ttype != qmod.TokenType.HOUSENUMBER: - repl.add_penalty(0.5 - tlist.tokens[0].penalty) - - - -def _dump_word_tokens(query: qmod.QueryStruct) -> Iterator[List[Any]]: - yield ['type', 'token', 'word_token', 'lookup_word', 'penalty', 'count', 'info', 'indexed'] - for node in query.nodes: - for tlist in node.starting: - for token in tlist.tokens: - t = cast(LegacyToken, token) - yield [tlist.ttype.name, t.token, t.word_token or '', - t.lookup_word or '', t.penalty, t.count, t.info, - 'Y' if t.is_indexed else 'N'] - - -async def create_query_analyzer(conn: SearchConnection) -> AbstractQueryAnalyzer: - """ Create and set up a new query analyzer for a database based - on the ICU tokenizer. - """ - out = LegacyQueryAnalyzer(conn) - await out.setup() - - return out diff --git a/src/nominatim_api/search/query.py b/src/nominatim_api/search/query.py index 04b7f1b8..53482df8 100644 --- a/src/nominatim_api/search/query.py +++ b/src/nominatim_api/search/query.py @@ -101,7 +101,6 @@ class Token(ABC): count: int addr_count: int lookup_word: str - is_indexed: bool @abstractmethod diff --git a/src/nominatim_api/version.py b/src/nominatim_api/version.py index d275f4fc..8c4de5a4 100644 --- a/src/nominatim_api/version.py +++ b/src/nominatim_api/version.py @@ -8,4 +8,4 @@ Version information for the Nominatim API. """ -NOMINATIM_API_VERSION = '4.4.99' +NOMINATIM_API_VERSION = '4.5.0' diff --git a/src/nominatim_db/cli.py b/src/nominatim_db/cli.py index 9fd439f8..8d8a07f7 100644 --- a/src/nominatim_db/cli.py +++ b/src/nominatim_db/cli.py @@ -19,7 +19,6 @@ from pathlib import Path from .config import Configuration from .errors import UsageError -from .tools.exec_utils import run_php_server from . import clicmd from . import version from .clicmd.args import NominatimArgs, Subcommand @@ -112,8 +111,7 @@ class CommandlineParser: args.config = Configuration(args.project_dir, environ=kwargs.get('environ', os.environ)) - args.config.set_libdirs(module=kwargs['module_dir'], - osm2pgsql=kwargs['osm2pgsql_path']) + args.config.set_libdirs(osm2pgsql=kwargs['osm2pgsql_path']) log = logging.getLogger() log.warning('Using project directory: %s', str(args.project_dir)) @@ -121,10 +119,6 @@ class CommandlineParser: try: ret = args.command.run(args) - if args.config.TOKENIZER == 'legacy': - log.warning('WARNING: the "legacy" tokenizer is deprecated ' - 'and will be removed in Nominatim 5.0.') - return ret except UsageError as exception: if log.isEnabledFor(logging.DEBUG): @@ -154,10 +148,10 @@ class AdminServe: from the current project directory. This webserver is only suitable for testing and development. Do not use it in production setups! - There are different webservers available. The default 'php' engine - runs the classic PHP frontend. The other engines are Python servers - which run the new Python frontend code. This is highly experimental - at the moment and may not include the full API. + There are two different webserver implementations for Python available: + falcon (the default) and starlette. You need to make sure the + appropriate Python packages as well as the uvicorn package are + installed to use this function. By the default, the webserver can be accessed at: http://127.0.0.1:8088 """ @@ -167,19 +161,12 @@ class AdminServe: group.add_argument('--server', default='127.0.0.1:8088', help='The address the server will listen to.') group.add_argument('--engine', default='falcon', - choices=('php', 'falcon', 'starlette'), + choices=('falcon', 'starlette'), help='Webserver framework to run. (default: falcon)') def run(self, args: NominatimArgs) -> int: - if args.engine == 'php': - if args.config.lib_dir.php is None: - raise UsageError("PHP frontend not configured.") - LOG.warning('\n\nWARNING: the PHP frontend is deprecated ' - 'and will be removed in Nominatim 5.0.\n\n') - run_php_server(args.server, args.project_dir / 'website') - else: - asyncio.run(self.run_uvicorn(args)) + asyncio.run(self.run_uvicorn(args)) return 0 diff --git a/src/nominatim_db/clicmd/refresh.py b/src/nominatim_db/clicmd/refresh.py index adc7ee65..74141165 100644 --- a/src/nominatim_db/clicmd/refresh.py +++ b/src/nominatim_db/clicmd/refresh.py @@ -69,7 +69,8 @@ class UpdateRefresh: group.add_argument('--importance', action='store_true', help='Recompute place importances (expensive!)') group.add_argument('--website', action='store_true', - help='Refresh the directory that serves the scripts for the web API') + help='DEPRECATED. This function has no function anymore' + ' and will be removed in a future version.') group.add_argument('--data-object', action='append', type=_parse_osm_object, metavar='OBJECT', help='Mark the given OSM object as requiring an update' @@ -159,14 +160,8 @@ class UpdateRefresh: refresh.recompute_importance(conn) if args.website: - webdir = args.project_dir / 'website' - LOG.warning('Setting up website directory at %s', webdir) - # This is a little bit hacky: call the tokenizer setup, so that - # the tokenizer directory gets repopulated as well, in case it - # wasn't there yet. - self._get_tokenizer(args.config) - with connect(args.config.get_libpq_dsn()) as conn: - refresh.setup_website(webdir, args.config, conn) + LOG.error('WARNING: Website setup is no longer required. ' + 'This function will be removed in future version of Nominatim.') if args.data_object or args.data_area: with connect(args.config.get_libpq_dsn()) as conn: diff --git a/src/nominatim_db/clicmd/setup.py b/src/nominatim_db/clicmd/setup.py index 07a76f59..a7066ff2 100644 --- a/src/nominatim_db/clicmd/setup.py +++ b/src/nominatim_db/clicmd/setup.py @@ -88,7 +88,7 @@ class SetupAll: async def async_run(self, args: NominatimArgs) -> int: from ..data import country_info - from ..tools import database_import, refresh, postcodes, freeze + from ..tools import database_import, postcodes, freeze from ..indexer.indexer import Indexer num_threads = args.threads or psutil.cpu_count() or 1 @@ -141,11 +141,6 @@ class SetupAll: LOG.warning('Recompute word counts') tokenizer.update_statistics(args.config, threads=num_threads) - webdir = args.project_dir / 'website' - LOG.warning('Setup website at %s', webdir) - with connect(args.config.get_libpq_dsn()) as conn: - refresh.setup_website(webdir, args.config, conn) - self._finalize_database(args.config.get_libpq_dsn(), args.offline) return 0 diff --git a/src/nominatim_db/config.py b/src/nominatim_db/config.py index 5ae3dea3..b220b5c7 100644 --- a/src/nominatim_db/config.py +++ b/src/nominatim_db/config.py @@ -59,20 +59,20 @@ class Configuration: other than string. """ - def __init__(self, project_dir: Optional[Path], + def __init__(self, project_dir: Optional[Union[Path, str]], environ: Optional[Mapping[str, str]] = None) -> None: - self.environ = environ or os.environ - self.project_dir = project_dir + self.environ = os.environ if environ is None else environ self.config_dir = paths.CONFIG_DIR self._config = dotenv_values(str(self.config_dir / 'env.defaults')) - if self.project_dir is not None and (self.project_dir / '.env').is_file(): - self.project_dir = self.project_dir.resolve() - self._config.update(dotenv_values(str(self.project_dir / '.env'))) + if project_dir is not None: + self.project_dir: Optional[Path] = Path(project_dir).resolve() + if (self.project_dir / '.env').is_file(): + self._config.update(dotenv_values(str(self.project_dir / '.env'))) + else: + self.project_dir = None class _LibDirs: - module: Path osm2pgsql: Path - php = paths.PHPLIB_DIR sql = paths.SQLLIB_DIR data = paths.DATA_DIR diff --git a/src/nominatim_db/paths.py b/src/nominatim_db/paths.py index aa289708..2614fa14 100644 --- a/src/nominatim_db/paths.py +++ b/src/nominatim_db/paths.py @@ -9,7 +9,6 @@ Path settings for extra data used by Nominatim. """ from pathlib import Path -PHPLIB_DIR = (Path(__file__) / '..' / '..' / '..' / 'lib-php').resolve() SQLLIB_DIR = (Path(__file__) / '..' / '..' / '..' / 'lib-sql').resolve() DATA_DIR = (Path(__file__) / '..' / '..' / '..' / 'data').resolve() CONFIG_DIR = (Path(__file__) / '..' / '..' / '..' / 'settings').resolve() diff --git a/src/nominatim_db/tokenizer/factory.py b/src/nominatim_db/tokenizer/factory.py index b9022d8d..5003c322 100644 --- a/src/nominatim_db/tokenizer/factory.py +++ b/src/nominatim_db/tokenizer/factory.py @@ -15,9 +15,6 @@ be used consistently when querying and updating the database. This module provides the functions to create and configure a new tokenizer as well as instantiating the appropriate tokenizer for updating an existing database. - -A tokenizer usually also includes PHP code for querying. The appropriate PHP -normalizer module is installed, when the tokenizer is created. """ from typing import Optional import logging diff --git a/src/nominatim_db/tokenizer/icu_tokenizer.py b/src/nominatim_db/tokenizer/icu_tokenizer.py index 4eee2c73..452bf26c 100644 --- a/src/nominatim_db/tokenizer/icu_tokenizer.py +++ b/src/nominatim_db/tokenizer/icu_tokenizer.py @@ -13,7 +13,6 @@ from typing import Optional, Sequence, List, Tuple, Mapping, Any, cast, \ import itertools import logging from pathlib import Path -from textwrap import dedent from psycopg.types.json import Jsonb from psycopg import sql as pysql @@ -64,7 +63,6 @@ class ICUTokenizer(AbstractTokenizer): """ self.loader = ICURuleLoader(config) - self._install_php(config.lib_dir.php, overwrite=True) self._save_config() if init_db: @@ -81,8 +79,6 @@ class ICUTokenizer(AbstractTokenizer): with connect(self.dsn) as conn: self.loader.load_config_from_db(conn) - self._install_php(config.lib_dir.php, overwrite=False) - def finalize_import(self, config: Configuration) -> None: """ Do any required postprocessing to make the tokenizer data ready @@ -282,22 +278,6 @@ class ICUTokenizer(AbstractTokenizer): return list(s[0].split('@')[0] for s in cur) - def _install_php(self, phpdir: Optional[Path], overwrite: bool = True) -> None: - """ Install the php script for the tokenizer. - """ - if phpdir is not None: - assert self.loader is not None - php_file = self.data_dir / "tokenizer.php" - - if not php_file.exists() or overwrite: - php_file.write_text(dedent(f"""\ - None: """ Save the configuration that needs to remain stable for the given database as database properties. diff --git a/src/nominatim_db/tokenizer/legacy_tokenizer.py b/src/nominatim_db/tokenizer/legacy_tokenizer.py deleted file mode 100644 index 04b7b881..00000000 --- a/src/nominatim_db/tokenizer/legacy_tokenizer.py +++ /dev/null @@ -1,686 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# -# This file is part of Nominatim. (https://nominatim.org) -# -# Copyright (C) 2024 by the Nominatim developer community. -# For a full list of authors see the git log. -""" -Tokenizer implementing normalisation as used before Nominatim 4. -""" -from typing import Optional, Sequence, List, Tuple, Mapping, Any, Callable, \ - cast, Dict, Set, Iterable -from collections import OrderedDict -import logging -from pathlib import Path -import re -import shutil -from textwrap import dedent - -from icu import Transliterator -import psycopg -from psycopg import sql as pysql - -from ..errors import UsageError -from ..db.connection import connect, Connection, drop_tables, table_exists,\ - execute_scalar, register_hstore -from ..config import Configuration -from ..db import properties -from ..db import utils as db_utils -from ..db.sql_preprocessor import SQLPreprocessor -from ..data.place_info import PlaceInfo -from .base import AbstractAnalyzer, AbstractTokenizer - -DBCFG_NORMALIZATION = "tokenizer_normalization" -DBCFG_MAXWORDFREQ = "tokenizer_maxwordfreq" - -LOG = logging.getLogger() - -def create(dsn: str, data_dir: Path) -> 'LegacyTokenizer': - """ Create a new instance of the tokenizer provided by this module. - """ - LOG.warning('WARNING: the legacy tokenizer is deprecated ' - 'and will be removed in Nominatim 5.0.') - return LegacyTokenizer(dsn, data_dir) - - -def _install_module(config_module_path: str, src_dir: Optional[Path], module_dir: Path) -> str: - """ Copies the PostgreSQL normalisation module into the project - directory if necessary. For historical reasons the module is - saved in the '/module' subdirectory and not with the other tokenizer - data. - - The function detects when the installation is run from the - build directory. It doesn't touch the module in that case. - """ - # Custom module locations are simply used as is. - if config_module_path: - LOG.info("Using custom path for database module at '%s'", config_module_path) - return config_module_path - - # Otherwise a source dir must be given. - if src_dir is None: - raise UsageError("The legacy tokenizer cannot be used with the Nominatim pip module.") - - # Compatibility mode for builddir installations. - if module_dir.exists() and src_dir.samefile(module_dir): - LOG.info('Running from build directory. Leaving database module as is.') - return str(module_dir) - - # In any other case install the module in the project directory. - if not module_dir.exists(): - module_dir.mkdir() - - destfile = module_dir / 'nominatim.so' - shutil.copy(str(src_dir / 'nominatim.so'), str(destfile)) - destfile.chmod(0o755) - - LOG.info('Database module installed at %s', str(destfile)) - - return str(module_dir) - - -def _check_module(module_dir: str, conn: Connection) -> None: - """ Try to use the PostgreSQL module to confirm that it is correctly - installed and accessible from PostgreSQL. - """ - with conn.cursor() as cur: - try: - cur.execute(pysql.SQL("""CREATE FUNCTION nominatim_test_import_func(text) - RETURNS text AS {}, 'transliteration' - LANGUAGE c IMMUTABLE STRICT; - DROP FUNCTION nominatim_test_import_func(text) - """).format(pysql.Literal(f'{module_dir}/nominatim.so'))) - except psycopg.DatabaseError as err: - LOG.fatal("Error accessing database module: %s", err) - raise UsageError("Database module cannot be accessed.") from err - - -class LegacyTokenizer(AbstractTokenizer): - """ The legacy tokenizer uses a special PostgreSQL module to normalize - names and queries. The tokenizer thus implements normalization through - calls to the database. - """ - - def __init__(self, dsn: str, data_dir: Path) -> None: - self.dsn = dsn - self.data_dir = data_dir - self.normalization: Optional[str] = None - - - def init_new_db(self, config: Configuration, init_db: bool = True) -> None: - """ Set up a new tokenizer for the database. - - This copies all necessary data in the project directory to make - sure the tokenizer remains stable even over updates. - """ - assert config.project_dir is not None - module_dir = _install_module(config.DATABASE_MODULE_PATH, - config.lib_dir.module, - config.project_dir / 'module') - - self.normalization = config.TERM_NORMALIZATION - - self._install_php(config, overwrite=True) - - with connect(self.dsn) as conn: - _check_module(module_dir, conn) - self._save_config(conn, config) - conn.commit() - - if init_db: - self.update_sql_functions(config) - self._init_db_tables(config) - - - def init_from_project(self, config: Configuration) -> None: - """ Initialise the tokenizer from the project directory. - """ - assert config.project_dir is not None - - with connect(self.dsn) as conn: - self.normalization = properties.get_property(conn, DBCFG_NORMALIZATION) - - if not (config.project_dir / 'module' / 'nominatim.so').exists(): - _install_module(config.DATABASE_MODULE_PATH, - config.lib_dir.module, - config.project_dir / 'module') - - self._install_php(config, overwrite=False) - - def finalize_import(self, config: Configuration) -> None: - """ Do any required postprocessing to make the tokenizer data ready - for use. - """ - with connect(self.dsn) as conn: - sqlp = SQLPreprocessor(conn, config) - sqlp.run_sql_file(conn, 'tokenizer/legacy_tokenizer_indices.sql') - - - def update_sql_functions(self, config: Configuration) -> None: - """ Reimport the SQL functions for this tokenizer. - """ - assert config.project_dir is not None - - with connect(self.dsn) as conn: - max_word_freq = properties.get_property(conn, DBCFG_MAXWORDFREQ) - modulepath = config.DATABASE_MODULE_PATH or \ - str((config.project_dir / 'module').resolve()) - sqlp = SQLPreprocessor(conn, config) - sqlp.run_sql_file(conn, 'tokenizer/legacy_tokenizer.sql', - max_word_freq=max_word_freq, - modulepath=modulepath) - - - def check_database(self, _: Configuration) -> Optional[str]: - """ Check that the tokenizer is set up correctly. - """ - hint = """\ - The Postgresql extension nominatim.so was not correctly loaded. - - Error: {error} - - Hints: - * Check the output of the CMmake/make installation step - * Does nominatim.so exist? - * Does nominatim.so exist on the database server? - * Can nominatim.so be accessed by the database user? - """ - with connect(self.dsn) as conn: - try: - out = execute_scalar(conn, "SELECT make_standard_name('a')") - except psycopg.Error as err: - return hint.format(error=str(err)) - - if out != 'a': - return hint.format(error='Unexpected result for make_standard_name()') - - return None - - - def migrate_database(self, config: Configuration) -> None: - """ Initialise the project directory of an existing database for - use with this tokenizer. - - This is a special migration function for updating existing databases - to new software versions. - """ - assert config.project_dir is not None - - self.normalization = config.TERM_NORMALIZATION - module_dir = _install_module(config.DATABASE_MODULE_PATH, - config.lib_dir.module, - config.project_dir / 'module') - - with connect(self.dsn) as conn: - _check_module(module_dir, conn) - self._save_config(conn, config) - - - def update_statistics(self, config: Configuration, threads: int = 1) -> None: - """ Recompute the frequency of full words. - """ - with connect(self.dsn) as conn: - if table_exists(conn, 'search_name'): - drop_tables(conn, "word_frequencies") - with conn.cursor() as cur: - LOG.info("Computing word frequencies") - cur.execute("""CREATE TEMP TABLE word_frequencies AS - SELECT unnest(name_vector) as id, count(*) - FROM search_name GROUP BY id""") - cur.execute("CREATE INDEX ON word_frequencies(id)") - LOG.info("Update word table with recomputed frequencies") - cur.execute("""UPDATE word SET search_name_count = count - FROM word_frequencies - WHERE word_token like ' %' and word_id = id""") - drop_tables(conn, "word_frequencies") - conn.commit() - - - def update_word_tokens(self) -> None: - """ No house-keeping implemented for the legacy tokenizer. - """ - LOG.info("No tokenizer clean-up available.") - - - def name_analyzer(self) -> 'LegacyNameAnalyzer': - """ Create a new analyzer for tokenizing names and queries - using this tokinzer. Analyzers are context managers and should - be used accordingly: - - ``` - with tokenizer.name_analyzer() as analyzer: - analyser.tokenize() - ``` - - When used outside the with construct, the caller must ensure to - call the close() function before destructing the analyzer. - - Analyzers are not thread-safe. You need to instantiate one per thread. - """ - normalizer = Transliterator.createFromRules("phrase normalizer", - self.normalization) - return LegacyNameAnalyzer(self.dsn, normalizer) - - - def most_frequent_words(self, conn: Connection, num: int) -> List[str]: - """ Return a list of the `num` most frequent full words - in the database. - """ - with conn.cursor() as cur: - cur.execute(""" SELECT word FROM word WHERE word is not null - ORDER BY search_name_count DESC LIMIT %s""", (num,)) - return list(s[0] for s in cur) - - - def _install_php(self, config: Configuration, overwrite: bool = True) -> None: - """ Install the php script for the tokenizer. - """ - if config.lib_dir.php is not None: - php_file = self.data_dir / "tokenizer.php" - - if not php_file.exists() or overwrite: - php_file.write_text(dedent(f"""\ - None: - """ Set up the word table and fill it with pre-computed word - frequencies. - """ - with connect(self.dsn) as conn: - sqlp = SQLPreprocessor(conn, config) - sqlp.run_sql_file(conn, 'tokenizer/legacy_tokenizer_tables.sql') - conn.commit() - - LOG.warning("Precomputing word tokens") - db_utils.execute_file(self.dsn, config.lib_dir.data / 'words.sql') - - - def _save_config(self, conn: Connection, config: Configuration) -> None: - """ Save the configuration that needs to remain stable for the given - database as database properties. - """ - assert self.normalization is not None - - properties.set_property(conn, DBCFG_NORMALIZATION, self.normalization) - properties.set_property(conn, DBCFG_MAXWORDFREQ, config.MAX_WORD_FREQUENCY) - - -class LegacyNameAnalyzer(AbstractAnalyzer): - """ The legacy analyzer uses the special Postgresql module for - splitting names. - - Each instance opens a connection to the database to request the - normalization. - """ - - def __init__(self, dsn: str, normalizer: Any): - self.conn: Optional[Connection] = connect(dsn) - self.conn.autocommit = True - self.normalizer = normalizer - register_hstore(self.conn) - - self._cache = _TokenCache(self.conn) - - - def close(self) -> None: - """ Free all resources used by the analyzer. - """ - if self.conn: - self.conn.close() - self.conn = None - - - def get_word_token_info(self, words: Sequence[str]) -> List[Tuple[str, str, int]]: - """ Return token information for the given list of words. - If a word starts with # it is assumed to be a full name - otherwise is a partial name. - - The function returns a list of tuples with - (original word, word token, word id). - - The function is used for testing and debugging only - and not necessarily efficient. - """ - assert self.conn is not None - with self.conn.cursor() as cur: - cur.execute("""SELECT t.term, word_token, word_id - FROM word, (SELECT unnest(%s::TEXT[]) as term) t - WHERE word_token = (CASE - WHEN left(t.term, 1) = '#' THEN - ' ' || make_standard_name(substring(t.term from 2)) - ELSE - make_standard_name(t.term) - END) - and class is null and country_code is null""", - (words, )) - - return [(r[0], r[1], r[2]) for r in cur] - - - def normalize(self, phrase: str) -> str: - """ Normalize the given phrase, i.e. remove all properties that - are irrelevant for search. - """ - return cast(str, self.normalizer.transliterate(phrase)) - - - def normalize_postcode(self, postcode: str) -> str: - """ Convert the postcode to a standardized form. - - This function must yield exactly the same result as the SQL function - 'token_normalized_postcode()'. - """ - return postcode.strip().upper() - - - def update_postcodes_from_db(self) -> None: - """ Update postcode tokens in the word table from the location_postcode - table. - """ - assert self.conn is not None - - with self.conn.cursor() as cur: - # This finds us the rows in location_postcode and word that are - # missing in the other table. - cur.execute("""SELECT * FROM - (SELECT pc, word FROM - (SELECT distinct(postcode) as pc FROM location_postcode) p - FULL JOIN - (SELECT word FROM word - WHERE class ='place' and type = 'postcode') w - ON pc = word) x - WHERE pc is null or word is null""") - - to_delete = [] - to_add = [] - - for postcode, word in cur: - if postcode is None: - to_delete.append(word) - else: - to_add.append(postcode) - - if to_delete: - cur.execute("""DELETE FROM WORD - WHERE class ='place' and type = 'postcode' - and word = any(%s) - """, (to_delete, )) - if to_add: - cur.execute("""SELECT count(create_postcode_id(pc)) - FROM unnest(%s::text[]) as pc - """, (to_add, )) - - - - def update_special_phrases(self, phrases: Iterable[Tuple[str, str, str, str]], - should_replace: bool) -> None: - """ Replace the search index for special phrases with the new phrases. - """ - assert self.conn is not None - - norm_phrases = set(((self.normalize(p[0]), p[1], p[2], p[3]) - for p in phrases)) - - with self.conn.cursor() as cur: - # Get the old phrases. - existing_phrases = set() - cur.execute("""SELECT word, class as cls, type, operator FROM word - WHERE class != 'place' - OR (type != 'house' AND type != 'postcode')""") - for label, cls, typ, oper in cur: - existing_phrases.add((label, cls, typ, oper or '-')) - - to_add = norm_phrases - existing_phrases - to_delete = existing_phrases - norm_phrases - - if to_add: - cur.executemany( - """ INSERT INTO word (word_id, word_token, word, class, type, - search_name_count, operator) - (SELECT nextval('seq_word'), ' ' || make_standard_name(name), name, - class, type, 0, - CASE WHEN op in ('in', 'near') THEN op ELSE null END - FROM (VALUES (%s, %s, %s, %s)) as v(name, class, type, op))""", - to_add) - - if to_delete and should_replace: - cur.executemany( - """ DELETE FROM word - USING (VALUES (%s, %s, %s, %s)) as v(name, in_class, in_type, op) - WHERE word = name and class = in_class and type = in_type - and ((op = '-' and operator is null) or op = operator)""", - to_delete) - - LOG.info("Total phrases: %s. Added: %s. Deleted: %s", - len(norm_phrases), len(to_add), len(to_delete)) - - - def add_country_names(self, country_code: str, names: Mapping[str, str]) -> None: - """ Add names for the given country to the search index. - """ - assert self.conn is not None - - with self.conn.cursor() as cur: - cur.execute( - """INSERT INTO word (word_id, word_token, country_code) - (SELECT nextval('seq_word'), lookup_token, %s - FROM (SELECT DISTINCT ' ' || make_standard_name(n) as lookup_token - FROM unnest(%s::TEXT[])n) y - WHERE NOT EXISTS(SELECT * FROM word - WHERE word_token = lookup_token and country_code = %s)) - """, (country_code, list(names.values()), country_code)) - - - def process_place(self, place: PlaceInfo) -> Mapping[str, Any]: - """ Determine tokenizer information about the given place. - - Returns a JSON-serialisable structure that will be handed into - the database via the token_info field. - """ - assert self.conn is not None - - token_info = _TokenInfo(self._cache) - - names = place.name - - if names: - token_info.add_names(self.conn, names) - - if place.is_country(): - assert place.country_code is not None - self.add_country_names(place.country_code, names) - - address = place.address - if address: - self._process_place_address(token_info, address) - - return token_info.data - - - def _process_place_address(self, token_info: '_TokenInfo', address: Mapping[str, str]) -> None: - assert self.conn is not None - hnrs = [] - addr_terms = [] - - for key, value in address.items(): - if key == 'postcode': - # Make sure the normalized postcode is present in the word table. - if re.search(r'[:,;]', value) is None: - norm_pc = self.normalize_postcode(value) - token_info.set_postcode(norm_pc) - self._cache.add_postcode(self.conn, norm_pc) - elif key in ('housenumber', 'streetnumber', 'conscriptionnumber'): - hnrs.append(value) - elif key == 'street': - token_info.add_street(self.conn, value) - elif key == 'place': - token_info.add_place(self.conn, value) - elif not key.startswith('_') \ - and key not in ('country', 'full', 'inclusion'): - addr_terms.append((key, value)) - - if hnrs: - token_info.add_housenumbers(self.conn, hnrs) - - if addr_terms: - token_info.add_address_terms(self.conn, addr_terms) - - - -class _TokenInfo: - """ Collect token information to be sent back to the database. - """ - def __init__(self, cache: '_TokenCache') -> None: - self.cache = cache - self.data: Dict[str, Any] = {} - - - def add_names(self, conn: Connection, names: Mapping[str, str]) -> None: - """ Add token information for the names of the place. - """ - # Create the token IDs for all names. - self.data['names'] = execute_scalar(conn, "SELECT make_keywords(%s)::text", - (names, )) - - - def add_housenumbers(self, conn: Connection, hnrs: Sequence[str]) -> None: - """ Extract housenumber information from the address. - """ - if len(hnrs) == 1: - token = self.cache.get_housenumber(hnrs[0]) - if token is not None: - self.data['hnr_tokens'] = token - self.data['hnr'] = hnrs[0] - return - - # split numbers if necessary - simple_list: List[str] = [] - for hnr in hnrs: - simple_list.extend((x.strip() for x in re.split(r'[;,]', hnr))) - - if len(simple_list) > 1: - simple_list = list(set(simple_list)) - - with conn.cursor() as cur: - cur.execute("SELECT * FROM create_housenumbers(%s)", (simple_list, )) - result = cur.fetchone() - assert result is not None - self.data['hnr_tokens'], self.data['hnr'] = result - - - def set_postcode(self, postcode: str) -> None: - """ Set or replace the postcode token with the given value. - """ - self.data['postcode'] = postcode - - def add_street(self, conn: Connection, street: str) -> None: - """ Add addr:street match terms. - """ - def _get_street(name: str) -> Optional[str]: - return cast(Optional[str], - execute_scalar(conn, "SELECT word_ids_from_name(%s)::text", (name, ))) - - tokens = self.cache.streets.get(street, _get_street) - self.data['street'] = tokens or '{}' - - - def add_place(self, conn: Connection, place: str) -> None: - """ Add addr:place search and match terms. - """ - def _get_place(name: str) -> Tuple[List[int], List[int]]: - with conn.cursor() as cur: - cur.execute("""SELECT make_keywords(hstore('name' , %s))::text, - word_ids_from_name(%s)::text""", - (name, name)) - return cast(Tuple[List[int], List[int]], cur.fetchone()) - - self.data['place_search'], self.data['place_match'] = \ - self.cache.places.get(place, _get_place) - - - def add_address_terms(self, conn: Connection, terms: Sequence[Tuple[str, str]]) -> None: - """ Add additional address terms. - """ - def _get_address_term(name: str) -> Tuple[List[int], List[int]]: - with conn.cursor() as cur: - cur.execute("""SELECT addr_ids_from_name(%s)::text, - word_ids_from_name(%s)::text""", - (name, name)) - return cast(Tuple[List[int], List[int]], cur.fetchone()) - - tokens = {} - for key, value in terms: - items = self.cache.address_terms.get(value, _get_address_term) - if items[0] or items[1]: - tokens[key] = items - - if tokens: - self.data['addr'] = tokens - - -class _LRU: - """ Least recently used cache that accepts a generator function to - produce the item when there is a cache miss. - """ - - def __init__(self, maxsize: int = 128): - self.data: 'OrderedDict[str, Any]' = OrderedDict() - self.maxsize = maxsize - - - def get(self, key: str, generator: Callable[[str], Any]) -> Any: - """ Get the item with the given key from the cache. If nothing - is found in the cache, generate the value through the - generator function and store it in the cache. - """ - value = self.data.get(key) - if value is not None: - self.data.move_to_end(key) - else: - value = generator(key) - if len(self.data) >= self.maxsize: - self.data.popitem(last=False) - self.data[key] = value - - return value - - -class _TokenCache: - """ Cache for token information to avoid repeated database queries. - - This cache is not thread-safe and needs to be instantiated per - analyzer. - """ - def __init__(self, conn: Connection): - # various LRU caches - self.streets = _LRU(maxsize=256) - self.places = _LRU(maxsize=128) - self.address_terms = _LRU(maxsize=1024) - - # Lookup houseunumbers up to 100 and cache them - with conn.cursor() as cur: - cur.execute("""SELECT i, ARRAY[getorcreate_housenumber_id(i::text)]::text - FROM generate_series(1, 100) as i""") - self._cached_housenumbers: Dict[str, str] = {str(r[0]): r[1] for r in cur} - - # For postcodes remember the ones that have already been added - self.postcodes: Set[str] = set() - - def get_housenumber(self, number: str) -> Optional[str]: - """ Get a housenumber token from the cache. - """ - return self._cached_housenumbers.get(number) - - - def add_postcode(self, conn: Connection, postcode: str) -> None: - """ Make sure the given postcode is in the database. - """ - if postcode not in self.postcodes: - with conn.cursor() as cur: - cur.execute('SELECT create_postcode_id(%s)', (postcode, )) - self.postcodes.add(postcode) diff --git a/src/nominatim_db/tools/convert_sqlite.py b/src/nominatim_db/tools/convert_sqlite.py index 2377abc0..d53527d1 100644 --- a/src/nominatim_db/tools/convert_sqlite.py +++ b/src/nominatim_db/tools/convert_sqlite.py @@ -7,7 +7,7 @@ """ Exporting a Nominatim database to SQlite. """ -from typing import Set, Any +from typing import Set, Any, Optional, Union import datetime as dt import logging from pathlib import Path @@ -21,7 +21,8 @@ from nominatim_api.sql.sqlalchemy_types import Geometry, IntArray LOG = logging.getLogger() -async def convert(project_dir: Path, outfile: Path, options: Set[str]) -> None: +async def convert(project_dir: Optional[Union[str, Path]], + outfile: Path, options: Set[str]) -> None: """ Export an existing database to sqlite. The resulting database will be usable against the Python frontend of Nominatim. """ diff --git a/src/nominatim_db/tools/exec_utils.py b/src/nominatim_db/tools/exec_utils.py index 1adcc777..4cbbf95d 100644 --- a/src/nominatim_db/tools/exec_utils.py +++ b/src/nominatim_db/tools/exec_utils.py @@ -14,19 +14,12 @@ import re import subprocess import shutil -from ..typing import StrPath from ..db.connection import get_pg_env from ..errors import UsageError from ..version import OSM2PGSQL_REQUIRED_VERSION LOG = logging.getLogger() -def run_php_server(server_address: str, base_dir: StrPath) -> None: - """ Run the built-in server from the given directory. - """ - subprocess.run(['/usr/bin/env', 'php', '-S', server_address], - cwd=str(base_dir), check=True) - def run_osm2pgsql(options: Mapping[str, Any]) -> None: """ Run osm2pgsql with the given options. diff --git a/src/nominatim_db/tools/migration.py b/src/nominatim_db/tools/migration.py index 54836532..12abe7fe 100644 --- a/src/nominatim_db/tools/migration.py +++ b/src/nominatim_db/tools/migration.py @@ -10,13 +10,11 @@ Functions for database migration to newer software versions. from typing import List, Tuple, Callable, Any import logging -from psycopg import sql as pysql - from ..errors import UsageError from ..config import Configuration from ..db import properties -from ..db.connection import connect, Connection, server_version_tuple,\ - table_has_column, table_exists, execute_scalar, register_hstore +from ..db.connection import connect, Connection,\ + table_exists, register_hstore from ..version import NominatimVersion, NOMINATIM_VERSION, parse_version from ..tokenizer import factory as tokenizer_factory from . import refresh @@ -38,19 +36,24 @@ def migrate(config: Configuration, paths: Any) -> int: if db_version_str is not None: db_version = parse_version(db_version_str) + else: + db_version = None - if db_version == NOMINATIM_VERSION: - LOG.warning("Database already at latest version (%s)", db_version_str) - return 0 + if db_version is None or db_version < (4, 3, 0, 0): + LOG.fatal('Your database version is older than 4.3. ' + 'Direct migration is not possible.\n' + 'You should strongly consider a reimport. If that is not possible\n' + 'please upgrade to 4.3 first and then to the newest version.') + raise UsageError('Migration not possible.') - LOG.info("Detected database version: %s", db_version_str) - else: - db_version = _guess_version(conn) + if db_version == NOMINATIM_VERSION: + LOG.warning("Database already at latest version (%s)", db_version_str) + return 0 + LOG.info("Detected database version: %s", db_version_str) for version, func in _MIGRATION_FUNCTIONS: - if db_version < version or \ - (db_version == (3, 5, 0, 99) and version == (3, 5, 0, 99)): + if db_version < version: title = func.__doc__ or '' LOG.warning("Running: %s (%s)", title.split('\n', 1)[0], version) kwargs = dict(conn=conn, config=config, paths=paths) @@ -69,25 +72,6 @@ def migrate(config: Configuration, paths: Any) -> int: return 0 -def _guess_version(conn: Connection) -> NominatimVersion: - """ Guess a database version when there is no property table yet. - Only migrations for 3.6 and later are supported, so bail out - when the version seems older. - """ - # In version 3.6, the country_name table was updated. Check for that. - cnt = execute_scalar(conn, """SELECT count(*) FROM - (SELECT svals(name) FROM country_name - WHERE country_code = 'gb')x; - """) - if cnt < 100: - LOG.fatal('It looks like your database was imported with a version ' - 'prior to 3.6.0. Automatic migration not possible.') - raise UsageError('Migration not possible.') - - return NominatimVersion(3, 5, 0, 99) - - - def _migration(major: int, minor: int, patch: int = 0, dbpatch: int = 0) -> Callable[[Callable[..., None]], Callable[..., None]]: """ Decorator for a single migration step. The parameters describe the @@ -110,273 +94,6 @@ def _migration(major: int, minor: int, patch: int = 0, return decorator -@_migration(3, 5, 0, 99) -def import_status_timestamp_change(conn: Connection, **_: Any) -> None: - """ Add timezone to timestamp in status table. - - The import_status table has been changed to include timezone information - with the time stamp. - """ - with conn.cursor() as cur: - cur.execute("""ALTER TABLE import_status ALTER COLUMN lastimportdate - TYPE timestamp with time zone;""") - - -@_migration(3, 5, 0, 99) -def add_nominatim_property_table(conn: Connection, config: Configuration, **_: Any) -> None: - """ Add nominatim_property table. - """ - if not table_exists(conn, 'nominatim_properties'): - with conn.cursor() as cur: - cur.execute(pysql.SQL("""CREATE TABLE nominatim_properties ( - property TEXT, - value TEXT); - GRANT SELECT ON TABLE nominatim_properties TO {}; - """).format(pysql.Identifier(config.DATABASE_WEBUSER))) - -@_migration(3, 6, 0, 0) -def change_housenumber_transliteration(conn: Connection, **_: Any) -> None: - """ Transliterate housenumbers. - - The database schema switched from saving raw housenumbers in - placex.housenumber to saving transliterated ones. - - Note: the function create_housenumber_id() has been dropped in later - versions. - """ - with conn.cursor() as cur: - cur.execute("""CREATE OR REPLACE FUNCTION create_housenumber_id(housenumber TEXT) - RETURNS TEXT AS $$ - DECLARE - normtext TEXT; - BEGIN - SELECT array_to_string(array_agg(trans), ';') - INTO normtext - FROM (SELECT lookup_word as trans, - getorcreate_housenumber_id(lookup_word) - FROM (SELECT make_standard_name(h) as lookup_word - FROM regexp_split_to_table(housenumber, '[,;]') h) x) y; - return normtext; - END; - $$ LANGUAGE plpgsql STABLE STRICT;""") - cur.execute("DELETE FROM word WHERE class = 'place' and type = 'house'") - cur.execute("""UPDATE placex - SET housenumber = create_housenumber_id(housenumber) - WHERE housenumber is not null""") - - -@_migration(3, 7, 0, 0) -def switch_placenode_geometry_index(conn: Connection, **_: Any) -> None: - """ Replace idx_placex_geometry_reverse_placeNode index. - - Make the index slightly more permissive, so that it can also be used - when matching up boundaries and place nodes. It makes the index - idx_placex_adminname index unnecessary. - """ - with conn.cursor() as cur: - cur.execute(""" CREATE INDEX IF NOT EXISTS idx_placex_geometry_placenode ON placex - USING GIST (geometry) - WHERE osm_type = 'N' and rank_search < 26 - and class = 'place' and type != 'postcode' - and linked_place_id is null""") - cur.execute(""" DROP INDEX IF EXISTS idx_placex_adminname """) - - -@_migration(3, 7, 0, 1) -def install_legacy_tokenizer(conn: Connection, config: Configuration, **_: Any) -> None: - """ Setup legacy tokenizer. - - If no other tokenizer has been configured yet, then create the - configuration for the backwards-compatible legacy tokenizer - """ - if properties.get_property(conn, 'tokenizer') is None: - for table in ('placex', 'location_property_osmline'): - if not table_has_column(conn, table, 'token_info'): - with conn.cursor() as cur: - cur.execute(pysql.SQL('ALTER TABLE {} ADD COLUMN token_info JSONB') - .format(pysql.Identifier(table))) - tokenizer = tokenizer_factory.create_tokenizer(config, init_db=False, - module_name='legacy') - - tokenizer.migrate_database(config) # type: ignore[attr-defined] - - -@_migration(4, 0, 99, 0) -def create_tiger_housenumber_index(conn: Connection, **_: Any) -> None: - """ Create idx_location_property_tiger_parent_place_id with included - house number. - - The inclusion is needed for efficient lookup of housenumbers in - full address searches. - """ - if server_version_tuple(conn) >= (11, 0, 0): - with conn.cursor() as cur: - cur.execute(""" CREATE INDEX IF NOT EXISTS - idx_location_property_tiger_housenumber_migrated - ON location_property_tiger - USING btree(parent_place_id) - INCLUDE (startnumber, endnumber) """) - - -@_migration(4, 0, 99, 1) -def create_interpolation_index_on_place(conn: Connection, **_: Any) -> None: - """ Create idx_place_interpolations for lookup of interpolation lines - on updates. - """ - with conn.cursor() as cur: - cur.execute("""CREATE INDEX IF NOT EXISTS idx_place_interpolations - ON place USING gist(geometry) - WHERE osm_type = 'W' and address ? 'interpolation'""") - - -@_migration(4, 0, 99, 2) -def add_step_column_for_interpolation(conn: Connection, **_: Any) -> None: - """ Add a new column 'step' to the interpolations table. - - Also converts the data into the stricter format which requires that - startnumbers comply with the odd/even requirements. - """ - if table_has_column(conn, 'location_property_osmline', 'step'): - return - - with conn.cursor() as cur: - # Mark invalid all interpolations with no intermediate numbers. - cur.execute("""UPDATE location_property_osmline SET startnumber = null - WHERE endnumber - startnumber <= 1 """) - # Align the start numbers where odd/even does not match. - cur.execute("""UPDATE location_property_osmline - SET startnumber = startnumber + 1, - linegeo = ST_LineSubString(linegeo, - 1.0 / (endnumber - startnumber)::float, - 1) - WHERE (interpolationtype = 'odd' and startnumber % 2 = 0) - or (interpolationtype = 'even' and startnumber % 2 = 1) - """) - # Mark invalid odd/even interpolations with no intermediate numbers. - cur.execute("""UPDATE location_property_osmline SET startnumber = null - WHERE interpolationtype in ('odd', 'even') - and endnumber - startnumber = 2""") - # Finally add the new column and populate it. - cur.execute("ALTER TABLE location_property_osmline ADD COLUMN step SMALLINT") - cur.execute("""UPDATE location_property_osmline - SET step = CASE WHEN interpolationtype = 'all' - THEN 1 ELSE 2 END - """) - - -@_migration(4, 0, 99, 3) -def add_step_column_for_tiger(conn: Connection, **_: Any) -> None: - """ Add a new column 'step' to the tiger data table. - """ - if table_has_column(conn, 'location_property_tiger', 'step'): - return - - with conn.cursor() as cur: - cur.execute("ALTER TABLE location_property_tiger ADD COLUMN step SMALLINT") - cur.execute("""UPDATE location_property_tiger - SET step = CASE WHEN interpolationtype = 'all' - THEN 1 ELSE 2 END - """) - - -@_migration(4, 0, 99, 4) -def add_derived_name_column_for_country_names(conn: Connection, **_: Any) -> None: - """ Add a new column 'derived_name' which in the future takes the - country names as imported from OSM data. - """ - if not table_has_column(conn, 'country_name', 'derived_name'): - with conn.cursor() as cur: - cur.execute("ALTER TABLE country_name ADD COLUMN derived_name public.HSTORE") - - -@_migration(4, 0, 99, 5) -def mark_internal_country_names(conn: Connection, config: Configuration, **_: Any) -> None: - """ Names from the country table should be marked as internal to prevent - them from being deleted. Only necessary for ICU tokenizer. - """ - tokenizer = tokenizer_factory.get_tokenizer_for_db(config) - with tokenizer.name_analyzer() as analyzer: - with conn.cursor() as cur: - cur.execute("SELECT country_code, name FROM country_name") - - for country_code, names in cur: - if not names: - names = {} - names['countrycode'] = country_code - analyzer.add_country_names(country_code, names) - - -@_migration(4, 1, 99, 0) -def add_place_deletion_todo_table(conn: Connection, **_: Any) -> None: - """ Add helper table for deleting data on updates. - - The table is only necessary when updates are possible, i.e. - the database is not in freeze mode. - """ - if table_exists(conn, 'place'): - with conn.cursor() as cur: - cur.execute("""CREATE TABLE IF NOT EXISTS place_to_be_deleted ( - osm_type CHAR(1), - osm_id BIGINT, - class TEXT, - type TEXT, - deferred BOOLEAN)""") - - -@_migration(4, 1, 99, 1) -def split_pending_index(conn: Connection, **_: Any) -> None: - """ Reorganise indexes for pending updates. - """ - if table_exists(conn, 'place'): - with conn.cursor() as cur: - cur.execute("""CREATE INDEX IF NOT EXISTS idx_placex_rank_address_sector - ON placex USING BTREE (rank_address, geometry_sector) - WHERE indexed_status > 0""") - cur.execute("""CREATE INDEX IF NOT EXISTS idx_placex_rank_boundaries_sector - ON placex USING BTREE (rank_search, geometry_sector) - WHERE class = 'boundary' and type = 'administrative' - and indexed_status > 0""") - cur.execute("DROP INDEX IF EXISTS idx_placex_pendingsector") - - -@_migration(4, 2, 99, 0) -def enable_forward_dependencies(conn: Connection, **_: Any) -> None: - """ Create indexes for updates with forward dependency tracking (long-running). - """ - if table_exists(conn, 'planet_osm_ways'): - with conn.cursor() as cur: - cur.execute("""SELECT * FROM pg_indexes - WHERE tablename = 'planet_osm_ways' - and indexdef LIKE '%nodes%'""") - if cur.rowcount == 0: - cur.execute("""CREATE OR REPLACE FUNCTION public.planet_osm_index_bucket(bigint[]) - RETURNS bigint[] - LANGUAGE sql IMMUTABLE - AS $function$ - SELECT ARRAY(SELECT DISTINCT unnest($1) >> 5) - $function$""") - cur.execute("""CREATE INDEX planet_osm_ways_nodes_bucket_idx - ON planet_osm_ways - USING gin (planet_osm_index_bucket(nodes)) - WITH (fastupdate=off)""") - cur.execute("""CREATE INDEX planet_osm_rels_parts_idx - ON planet_osm_rels USING gin (parts) - WITH (fastupdate=off)""") - cur.execute("ANALYZE planet_osm_ways") - - -@_migration(4, 2, 99, 1) -def add_improved_geometry_reverse_placenode_index(conn: Connection, **_: Any) -> None: - """ Create improved index for reverse lookup of place nodes. - """ - with conn.cursor() as cur: - cur.execute("""CREATE INDEX IF NOT EXISTS idx_placex_geometry_reverse_lookupPlaceNode - ON placex - USING gist (ST_Buffer(geometry, reverse_place_diameter(rank_search))) - WHERE rank_address between 4 and 25 AND type != 'postcode' - AND name is not null AND linked_place_id is null AND osm_type = 'N' - """) - @_migration(4, 4, 99, 0) def create_postcode_area_lookup_index(conn: Connection, **_: Any) -> None: """ Create index needed for looking up postcode areas from postocde points. diff --git a/src/nominatim_db/tools/refresh.py b/src/nominatim_db/tools/refresh.py index d48c4e45..557c43ae 100644 --- a/src/nominatim_db/tools/refresh.py +++ b/src/nominatim_db/tools/refresh.py @@ -11,17 +11,15 @@ from typing import MutableSequence, Tuple, Any, Type, Mapping, Sequence, List, c import csv import gzip import logging -from textwrap import dedent from pathlib import Path from psycopg import sql as pysql from ..config import Configuration from ..db.connection import Connection, connect, postgis_version_tuple,\ - drop_tables, table_exists + drop_tables from ..db.utils import execute_file from ..db.sql_preprocessor import SQLPreprocessor -from ..version import NOMINATIM_VERSION LOG = logging.getLogger() @@ -99,34 +97,6 @@ def create_functions(conn: Connection, config: Configuration, debug=enable_debug) - -WEBSITE_SCRIPTS = ( - 'deletable.php', - 'details.php', - 'lookup.php', - 'polygons.php', - 'reverse.php', - 'search.php', - 'status.php' -) - -# constants needed by PHP scripts: PHP name, config name, type -PHP_CONST_DEFS = ( - ('Database_DSN', 'DATABASE_DSN', str), - ('Default_Language', 'DEFAULT_LANGUAGE', str), - ('Log_DB', 'LOG_DB', bool), - ('Log_File', 'LOG_FILE', Path), - ('NoAccessControl', 'CORS_NOACCESSCONTROL', bool), - ('Places_Max_ID_count', 'LOOKUP_MAX_COUNT', int), - ('PolygonOutput_MaximumTypes', 'POLYGON_OUTPUT_MAX_TYPES', int), - ('Search_BatchMode', 'SEARCH_BATCH_MODE', bool), - ('Search_NameOnlySearchFrequencyThreshold', 'SEARCH_NAME_ONLY_THRESHOLD', str), - ('Use_US_Tiger_Data', 'USE_US_TIGER_DATA', bool), - ('MapIcon_URL', 'MAPICON_URL', str), - ('Search_WithinCountries', 'SEARCH_WITHIN_COUNTRIES', bool), -) - - def import_wikipedia_articles(dsn: str, data_path: Path, ignore_errors: bool = False) -> int: """ Replaces the wikipedia importance tables with new data. The import is run in a single transaction so that the new data @@ -272,46 +242,6 @@ def _quote_php_variable(var_type: Type[Any], config: Configuration, return f"'{quoted}'" -def setup_website(basedir: Path, config: Configuration, conn: Connection) -> None: - """ Create the website script stubs. - """ - if config.lib_dir.php is None: - LOG.info("Python frontend does not require website setup. Skipping.") - return - - if not basedir.exists(): - LOG.info('Creating website directory.') - basedir.mkdir() - - assert config.project_dir is not None - basedata = dedent(f"""\ - None: """ Mark the given OSM object for reindexing. When 'recursive' is set diff --git a/src/nominatim_db/version.py b/src/nominatim_db/version.py index 588a31c8..8cc8e4fe 100644 --- a/src/nominatim_db/version.py +++ b/src/nominatim_db/version.py @@ -58,7 +58,7 @@ def parse_version(version: str) -> NominatimVersion: return NominatimVersion(*[int(x) for x in parts[:2] + parts[2].split('-')]) -NOMINATIM_VERSION = parse_version('4.4.99-1') +NOMINATIM_VERSION = parse_version('4.5.0-0') POSTGRESQL_REQUIRED_VERSION = (9, 6) POSTGIS_REQUIRED_VERSION = (2, 2) diff --git a/test/Makefile b/test/Makefile index 5f78eeac..9768ebd7 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,14 +1,10 @@ -all: bdd php python -no-test-db: bdd-no-test-db php +all: bdd python bdd: cd bdd && behave -DREMOVE_TEMPLATE=1 -php: - cd php && phpunit ./ - python: pytest python -.PHONY: bdd php no-test-db python +.PHONY: bdd python diff --git a/test/bdd/api/details/simple.feature b/test/bdd/api/details/simple.feature index 0e456aa5..5e0bacc5 100644 --- a/test/bdd/api/details/simple.feature +++ b/test/bdd/api/details/simple.feature @@ -42,16 +42,6 @@ Feature: Object details | N300209696:highway | - @v1-api-php-only - Scenario: Details for interpolation way just return the dependent street - When sending details query for W1 - Then the result is valid json - And results contain - | category | - | highway | - - - @v1-api-python-only Scenario: Details for interpolation way return the interpolation When sending details query for W1 Then the result is valid json @@ -60,17 +50,6 @@ Feature: Object details | place | houses | W | 1 | 15 | - @v1-api-php-only - @Fail - Scenario: Details for Tiger way just return the dependent street - When sending details query for 112871 - Then the result is valid json - And results contain - | category | - | highway | - - - @v1-api-python-only @Fail Scenario: Details for interpolation way return the interpolation When sending details query for 112871 @@ -81,17 +60,6 @@ Feature: Object details And result has not attributes osm_type,osm_id - @v1-api-php-only - @Fail - Scenario: Details for postcodes just return the dependent place - When sending details query for 112820 - Then the result is valid json - And results contain - | category | - | boundary | - - - @v1-api-python-only @Fail Scenario: Details for interpolation way return the interpolation When sending details query for 112820 @@ -102,7 +70,6 @@ Feature: Object details And result has not attributes osm_type,osm_id - @v1-api-python-only Scenario Outline: Details debug output returns no errors When sending debug details query for Then the result is valid html diff --git a/test/bdd/api/reverse/layers.feature b/test/bdd/api/reverse/layers.feature index ef028864..f1885f0e 100644 --- a/test/bdd/api/reverse/layers.feature +++ b/test/bdd/api/reverse/layers.feature @@ -3,7 +3,6 @@ Feature: Layer parameter in reverse geocoding Testing correct function of layer selection while reverse geocoding - @v1-api-python-only Scenario: POIs are selected by default When sending v1/reverse at 47.14077,9.52414 Then results contain @@ -11,7 +10,6 @@ Feature: Layer parameter in reverse geocoding | tourism | viewpoint | - @v1-api-python-only Scenario Outline: Same address level POI with different layers When sending v1/reverse at 47.14077,9.52414 | layer | @@ -31,7 +29,6 @@ Feature: Layer parameter in reverse geocoding | natural,poi | tourism | - @v1-api-python-only Scenario Outline: POIs are not selected without housenumber for address layer When sending v1/reverse at 47.13816,9.52168 | layer | @@ -46,7 +43,6 @@ Feature: Layer parameter in reverse geocoding | address | amenity | parking | - @v1-api-python-only Scenario: Between natural and low-zoom address prefer natural When sending v1/reverse at 47.13636,9.52094 | layer | zoom | @@ -56,7 +52,6 @@ Feature: Layer parameter in reverse geocoding | waterway | - @v1-api-python-only Scenario Outline: Search for mountain peaks begins at level 12 When sending v1/reverse at 47.08293,9.57109 | layer | zoom | @@ -71,7 +66,6 @@ Feature: Layer parameter in reverse geocoding | 13 | waterway | river | - @v1-api-python-only Scenario Outline: Reverse search with manmade layers When sending v1/reverse at 32.46904,-86.44439 | layer | diff --git a/test/bdd/api/reverse/v1_params.feature b/test/bdd/api/reverse/v1_params.feature index a1f08afd..09a190ed 100644 --- a/test/bdd/api/reverse/v1_params.feature +++ b/test/bdd/api/reverse/v1_params.feature @@ -18,20 +18,6 @@ Feature: v1/reverse Parameter Tests When sending v1/reverse at ,52.52 Then a HTTP 400 is returned - @v1-api-php-only - Scenario: Missing osm_id parameter - When sending v1/reverse at , - | osm_type | - | N | - Then a HTTP 400 is returned - - @v1-api-php-only - Scenario: Missing osm_type parameter - When sending v1/reverse at , - | osm_id | - | 3498564 | - Then a HTTP 400 is returned - Scenario Outline: Bad format for lat or lon When sending v1/reverse at , @@ -151,7 +137,6 @@ Feature: v1/reverse Parameter Tests | foo; evil | - @v1-api-python-only Scenario Outline: Reverse debug mode produces valid HTML When sending v1/reverse at , with format debug | lat | lon | diff --git a/test/bdd/api/search/params.feature b/test/bdd/api/search/params.feature index e667b690..e77a00d2 100644 --- a/test/bdd/api/search/params.feature +++ b/test/bdd/api/search/params.feature @@ -69,7 +69,6 @@ Feature: Search queries | 0 | Then there are duplicates - @fail-legacy Scenario: Search with bounded viewbox in right area When sending json search query "post" with address | bounded | viewbox | @@ -353,19 +352,6 @@ Feature: Search queries | svg | | geokml | - @v1-api-php-only - Scenario: Search along a route - When sending json search query "rathaus" with address - Then result addresses contain - | ID | town | - | 0 | Schaan | - When sending json search query "rathaus" with address - | bounded | routewidth | route | - | 1 | 0.1 | 9.54353,47.11772,9.54314,47.11894 | - Then result addresses contain - | town | - | Triesenberg | - Scenario: Array parameters are ignored When sending json search query "Vaduz" with address diff --git a/test/bdd/api/search/postcode.feature b/test/bdd/api/search/postcode.feature index 827af1ea..bb1b755b 100644 --- a/test/bdd/api/search/postcode.feature +++ b/test/bdd/api/search/postcode.feature @@ -3,7 +3,7 @@ Feature: Searches with postcodes Various searches involving postcodes - @v1-api-php-only + @Fail Scenario: US 5+4 ZIP codes are shortened to 5 ZIP codes if not found When sending json search query "36067 1111, us" with address Then result addresses contain diff --git a/test/bdd/api/search/queries.feature b/test/bdd/api/search/queries.feature index b2793faa..6e640acc 100644 --- a/test/bdd/api/search/queries.feature +++ b/test/bdd/api/search/queries.feature @@ -106,17 +106,6 @@ Feature: Search queries | class | type | | club | scout | - @v1-api-php-only - Scenario: With multiple amenity search only the first is used - When sending json search query "[club=scout] [church] vaduz" - Then results contain - | class | type | - | club | scout | - When sending json search query "[amenity=place_of_worship] [club=scout] vaduz" - Then results contain - | class | type | - | amenity | place_of_worship | - Scenario: POI search near given coordinate When sending json search query "restaurant near 47.16712,9.51100" Then results contain @@ -129,13 +118,6 @@ Feature: Search queries | class | type | | leisure | firepit | - @v1-api-php-only - Scenario: Arbitrary key/value search near given coordinate and named place - When sending json search query "[leisure=firepit] ebenholz 47° 9′ 26″ N 9° 36′ 45″ E" - Then results contain - | class | type | - | leisure | firepit | - Scenario: POI search in a bounded viewbox When sending json search query "restaurants" diff --git a/test/bdd/db/import/parenting.feature b/test/bdd/db/import/parenting.feature index c349b69f..55fa6a60 100644 --- a/test/bdd/db/import/parenting.feature +++ b/test/bdd/db/import/parenting.feature @@ -104,7 +104,6 @@ Feature: Parenting of objects | N3 | W2 | | N4 | W1 | - @fail-legacy Scenario: addr:street tag parents to appropriately named street, locale names Given the grid | 10 | | | | | 11 | diff --git a/test/bdd/db/import/postcodes.feature b/test/bdd/db/import/postcodes.feature index 57f90d98..3f4976f1 100644 --- a/test/bdd/db/import/postcodes.feature +++ b/test/bdd/db/import/postcodes.feature @@ -195,7 +195,6 @@ Feature: Import of postcodes | E45 2 | gb | 23 | 5 | | Y45 | gb | 21 | 5 | - @fail-legacy Scenario: Postcodes outside all countries are not added to the postcode and word table Given the places | osm | class | type | addr+postcode | addr+housenumber | addr+place | geometry | diff --git a/test/bdd/db/import/search_name.feature b/test/bdd/db/import/search_name.feature index 538bcbb3..cd581c46 100644 --- a/test/bdd/db/import/search_name.feature +++ b/test/bdd/db/import/search_name.feature @@ -11,7 +11,6 @@ Feature: Creation of search terms | object | name_vector | | N1 | #New York, #Big Apple | - @fail-legacy Scenario: Comma-separated names appear as a single full name Given the places | osm | class | type | name+alt_name | diff --git a/test/bdd/db/query/housenumbers.feature b/test/bdd/db/query/housenumbers.feature index 106bc8bb..16d9fd5a 100644 --- a/test/bdd/db/query/housenumbers.feature +++ b/test/bdd/db/query/housenumbers.feature @@ -27,7 +27,6 @@ Feature: Searching of house numbers | N1 | - @fail-legacy Scenario Outline: Numeral housenumbers in any script are found Given the places | osm | class | type | housenr | geometry | @@ -84,7 +83,6 @@ Feature: Searching of house numbers | 2, 4, 12 | - @fail-legacy Scenario Outline: Housenumber - letter combinations are found Given the places | osm | class | type | housenr | geometry | @@ -150,7 +148,6 @@ Feature: Searching of house numbers | 34/10 | - @fail-legacy Scenario Outline: a bis housenumber is found Given the places | osm | class | type | housenr | geometry | @@ -184,7 +181,6 @@ Feature: Searching of house numbers | 45 bis | - @fail-legacy Scenario Outline: a ter housenumber is found Given the places | osm | class | type | housenr | geometry | @@ -218,7 +214,6 @@ Feature: Searching of house numbers | 45 TER | - @fail-legacy Scenario Outline: a number - letter - number combination housenumber is found Given the places | osm | class | type | housenr | geometry | @@ -252,7 +247,6 @@ Feature: Searching of house numbers | 501h1 | - @fail-legacy Scenario Outline: Russian housenumbers are found Given the places | osm | class | type | housenr | geometry | diff --git a/test/bdd/db/query/japanese.feature b/test/bdd/db/query/japanese.feature index 4960c50b..f21e0f5c 100644 --- a/test/bdd/db/query/japanese.feature +++ b/test/bdd/db/query/japanese.feature @@ -1,7 +1,6 @@ @DB Feature: Searches in Japan Test specifically for searches of Japanese addresses and in Japanese language. - @fail-legacy Scenario: A block house-number is parented to the neighbourhood Given the grid with origin JP | 1 | | | | 2 | diff --git a/test/bdd/db/query/postcodes.feature b/test/bdd/db/query/postcodes.feature index fa4f6a0b..e8a2ccc2 100644 --- a/test/bdd/db/query/postcodes.feature +++ b/test/bdd/db/query/postcodes.feature @@ -14,7 +14,6 @@ Feature: Querying fo postcode variants | 0 | postcode | 399174, Singapore | - @fail-legacy Scenario Outline: Postcodes in the Netherlands (mixed postcode with spaces) Given the grid with origin NL | 10 | | | | 11 | @@ -38,7 +37,6 @@ Feature: Querying fo postcode variants | 3993 dx | - @fail-legacy Scenario: Postcodes in Singapore (6-digit postcode) Given the grid with origin SG | 10 | | | | 11 | @@ -52,7 +50,6 @@ Feature: Querying fo postcode variants | 0 | postcode | 399174, Singapore | - @fail-legacy Scenario Outline: Postcodes in Andorra (with country code) Given the grid with origin AD | 10 | | | | 11 | @@ -76,7 +73,6 @@ Feature: Querying fo postcode variants | AD675 | - @fail-legacy Scenario: Different postcodes with the same normalization can both be found Given the places | osm | class | type | addr+postcode | addr+housenumber | geometry | @@ -97,8 +93,6 @@ Feature: Querying fo postcode variants | postcode | E4 7EA, United Kingdom | - @fail-legacy - @v1-api-python-only Scenario: Postcode areas are preferred over postcode points Given the grid with origin DE | 1 | 2 | diff --git a/test/bdd/db/query/reverse.feature b/test/bdd/db/query/reverse.feature index 12941102..11ee8685 100644 --- a/test/bdd/db/query/reverse.feature +++ b/test/bdd/db/query/reverse.feature @@ -2,7 +2,6 @@ Feature: Reverse searches Test results of reverse queries - @v1-api-python-only Scenario: POI in POI area Given the 0.0001 grid with origin 1,1 | 1 | | | | | | | | 2 | diff --git a/test/bdd/db/query/search_simple.feature b/test/bdd/db/query/search_simple.feature index 270d2e55..5fef3132 100644 --- a/test/bdd/db/query/search_simple.feature +++ b/test/bdd/db/query/search_simple.feature @@ -77,7 +77,6 @@ Feature: Searching of simple objects | W1 | - @fail-legacy Scenario Outline: Special cased american states will be found Given the grid | 1 | | 2 | diff --git a/test/bdd/db/update/country.feature b/test/bdd/db/update/country.feature index 794b0d0e..abc1af09 100644 --- a/test/bdd/db/update/country.feature +++ b/test/bdd/db/update/country.feature @@ -8,7 +8,6 @@ Feature: Country handling | | 10 | | | 4 | | 3 | - @fail-legacy Scenario: When country names are changed old ones are no longer searchable Given the places | osm | class | type | admin | name+name:xy | country | geometry | @@ -27,7 +26,6 @@ Feature: Country handling When sending search query "Wenig, Loudou" Then exactly 0 results are returned - @fail-legacy Scenario: When country names are deleted they are no longer searchable Given the places | osm | class | type | admin | name+name:xy | country | geometry | @@ -83,7 +81,6 @@ Feature: Country handling | N10 | Wenig, Lilly | - @fail-legacy Scenario: When a localised name is deleted, the standard name takes over Given the places | osm | class | type | admin | name+name:de | country | geometry | diff --git a/test/bdd/environment.py b/test/bdd/environment.py index 155b8d90..7535c508 100644 --- a/test/bdd/environment.py +++ b/test/bdd/environment.py @@ -27,7 +27,6 @@ userconfig = { 'TEST_DB' : 'test_nominatim', 'API_TEST_DB' : 'test_api_nominatim', 'API_TEST_FILE' : TEST_BASE_DIR / 'testdb' / 'apidb-test-data.pbf', - 'SERVER_MODULE_PATH' : None, 'TOKENIZER' : None, # Test with a custom tokenizer 'STYLE' : 'extratags', 'API_ENGINE': 'falcon' @@ -60,15 +59,3 @@ def before_scenario(context, scenario): def after_scenario(context, scenario): if 'DB' in context.tags: context.nominatim.teardown_db(context) - - -def before_tag(context, tag): - if tag == 'fail-legacy': - if context.config.userdata['TOKENIZER'] == 'legacy': - context.scenario.skip("Not implemented in legacy tokenizer") - if tag == 'v1-api-php-only': - if context.config.userdata['API_ENGINE'] != 'php': - context.scenario.skip("Only valid with PHP version of v1 API.") - if tag == 'v1-api-python-only': - if context.config.userdata['API_ENGINE'] == 'php': - context.scenario.skip("Only valid with Python version of v1 API.") diff --git a/test/bdd/steps/nominatim_environment.py b/test/bdd/steps/nominatim_environment.py index c4b05588..ba19bb48 100644 --- a/test/bdd/steps/nominatim_environment.py +++ b/test/bdd/steps/nominatim_environment.py @@ -34,7 +34,6 @@ class NominatimEnvironment: self.api_test_file = config['API_TEST_FILE'] self.tokenizer = config['TOKENIZER'] self.import_style = config['STYLE'] - self.server_module_path = config['SERVER_MODULE_PATH'] self.reuse_template = not config['REMOVE_TEMPLATE'] self.keep_scenario_db = config['KEEP_TEST_DB'] @@ -44,14 +43,9 @@ class NominatimEnvironment: self.api_db_done = False self.website_dir = None - self.api_engine = None - if config['API_ENGINE'] != 'php': - if not hasattr(self, f"create_api_request_func_{config['API_ENGINE']}"): - raise RuntimeError(f"Unknown API engine '{config['API_ENGINE']}'") - self.api_engine = getattr(self, f"create_api_request_func_{config['API_ENGINE']}")() - - if self.tokenizer == 'legacy' and self.server_module_path is None: - raise RuntimeError("You must set -DSERVER_MODULE_PATH when testing the legacy tokenizer.") + if not hasattr(self, f"create_api_request_func_{config['API_ENGINE']}"): + raise RuntimeError(f"Unknown API engine '{config['API_ENGINE']}'") + self.api_engine = getattr(self, f"create_api_request_func_{config['API_ENGINE']}")() def connect_database(self, dbname): """ Return a connection to the database with the given name. @@ -102,27 +96,14 @@ class NominatimEnvironment: if self.import_style is not None: self.test_env['NOMINATIM_IMPORT_STYLE'] = self.import_style - if self.server_module_path: - self.test_env['NOMINATIM_DATABASE_MODULE_PATH'] = self.server_module_path - if self.website_dir is not None: self.website_dir.cleanup() self.website_dir = tempfile.TemporaryDirectory() - try: - conn = self.connect_database(dbname) - except: - conn = False - refresh.setup_website(Path(self.website_dir.name) / 'website', - self.get_test_config(), conn) - if conn: - conn.close() - def get_test_config(self): cfg = Configuration(Path(self.website_dir.name), environ=self.test_env) - cfg.set_libdirs(module=self.server_module_path) return cfg def get_libpq_dsn(self): @@ -201,12 +182,8 @@ class NominatimEnvironment: self.run_nominatim('add-data', '--tiger-data', str(testdata / 'tiger')) self.run_nominatim('freeze') - if self.tokenizer == 'legacy': - phrase_file = str(testdata / 'specialphrases_testdb.sql') - run_script(['psql', '-d', self.api_test_db, '-f', phrase_file]) - else: - csv_path = str(testdata / 'full_en_phrases_test.csv') - self.run_nominatim('special-phrases', '--import-from-csv', csv_path) + csv_path = str(testdata / 'full_en_phrases_test.csv') + self.run_nominatim('special-phrases', '--import-from-csv', csv_path) except: self.db_drop_database(self.api_test_db) raise @@ -289,8 +266,7 @@ class NominatimEnvironment: if self.website_dir is not None: cmdline = list(cmdline) + ['--project-dir', self.website_dir.name] - cli.nominatim(module_dir=self.server_module_path, - osm2pgsql_path=None, + cli.nominatim(osm2pgsql_path=None, cli_args=cmdline, environ=self.test_env) diff --git a/test/bdd/steps/steps_api_queries.py b/test/bdd/steps/steps_api_queries.py index 93501e42..4d15381d 100644 --- a/test/bdd/steps/steps_api_queries.py +++ b/test/bdd/steps/steps_api_queries.py @@ -1,13 +1,10 @@ -# SPDX-License-Identifier: GPL-2.0-only +# SPDX-License-Identifier: GPL-3.0-or-later # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2022 by the Nominatim developer community. +# Copyright (C) 2024 by the Nominatim developer community. # For a full list of authors see the git log. """ Steps that run queries against the API. - - Queries may either be run directly via PHP using the query script - or via the HTTP interface using php-cgi. """ from pathlib import Path import json @@ -25,29 +22,6 @@ from table_compare import NominatimID LOG = logging.getLogger(__name__) -BASE_SERVER_ENV = { - 'HTTP_HOST' : 'localhost', - 'HTTP_USER_AGENT' : 'Mozilla/5.0 (X11; Linux x86_64; rv:51.0) Gecko/20100101 Firefox/51.0', - 'HTTP_ACCEPT' : 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'HTTP_ACCEPT_ENCODING' : 'gzip, deflate', - 'HTTP_CONNECTION' : 'keep-alive', - 'SERVER_SIGNATURE' : '
Nominatim BDD Tests
', - 'SERVER_SOFTWARE' : 'Nominatim test', - 'SERVER_NAME' : 'localhost', - 'SERVER_ADDR' : '127.0.1.1', - 'SERVER_PORT' : '80', - 'REMOTE_ADDR' : '127.0.0.1', - 'DOCUMENT_ROOT' : '/var/www', - 'REQUEST_SCHEME' : 'http', - 'CONTEXT_PREFIX' : '/', - 'SERVER_ADMIN' : 'webmaster@localhost', - 'REMOTE_PORT' : '49319', - 'GATEWAY_INTERFACE' : 'CGI/1.1', - 'SERVER_PROTOCOL' : 'HTTP/1.1', - 'REQUEST_METHOD' : 'GET', - 'REDIRECT_STATUS' : 'CGI' -} - def make_todo_list(context, result_id): if result_id is None: @@ -88,50 +62,12 @@ def send_api_query(endpoint, params, fmt, context): for h in context.table.headings: params[h] = context.table[0][h] - if context.nominatim.api_engine is None: - return send_api_query_php(endpoint, params, context) - return asyncio.run(context.nominatim.api_engine(endpoint, params, Path(context.nominatim.website_dir.name), context.nominatim.test_env, getattr(context, 'http_headers', {}))) - -def send_api_query_php(endpoint, params, context): - env = dict(BASE_SERVER_ENV) - env['QUERY_STRING'] = urlencode(params) - - env['SCRIPT_NAME'] = f'/{endpoint}.php' - env['REQUEST_URI'] = f"{env['SCRIPT_NAME']}?{env['QUERY_STRING']}" - env['CONTEXT_DOCUMENT_ROOT'] = os.path.join(context.nominatim.website_dir.name, 'website') - env['SCRIPT_FILENAME'] = os.path.join(env['CONTEXT_DOCUMENT_ROOT'], - f'{endpoint}.php') - - LOG.debug("Environment:" + json.dumps(env, sort_keys=True, indent=2)) - - if hasattr(context, 'http_headers'): - for k, v in context.http_headers.items(): - env['HTTP_' + k.upper().replace('-', '_')] = v - - cmd = ['/usr/bin/env', 'php-cgi', '-f', env['SCRIPT_FILENAME']] - - for k,v in params.items(): - cmd.append(f"{k}={v}") - - outp, err = run_script(cmd, cwd=context.nominatim.website_dir.name, env=env) - - assert len(err) == 0, f"Unexpected PHP error: {err}" - - if outp.startswith('Status: '): - status = int(outp[8:11]) - else: - status = 200 - - content_start = outp.find('\r\n\r\n') - - return outp[content_start + 4:], status - @given(u'the HTTP header') def add_http_header(context): if not hasattr(context, 'http_headers'): diff --git a/test/bdd/steps/steps_db_ops.py b/test/bdd/steps/steps_db_ops.py index a0dd9b34..fb8431d5 100644 --- a/test/bdd/steps/steps_db_ops.py +++ b/test/bdd/steps/steps_db_ops.py @@ -28,9 +28,8 @@ def check_database_integrity(context): assert cur.fetchone()[0] == 0, "Duplicates found in place_addressline" # word table must not have empty word_tokens - if context.nominatim.tokenizer != 'legacy': - cur.execute("SELECT count(*) FROM word WHERE word_token = ''") - assert cur.fetchone()[0] == 0, "Empty word tokens found in word table" + cur.execute("SELECT count(*) FROM word WHERE word_token = ''") + assert cur.fetchone()[0] == 0, "Empty word tokens found in word table" @@ -324,13 +323,8 @@ def check_word_table_for_postcodes(context, exclude, postcodes): plist.sort() with context.db.cursor() as cur: - if nctx.tokenizer != 'legacy': - cur.execute("SELECT word FROM word WHERE type = 'P' and word = any(%s)", - (plist,)) - else: - cur.execute("""SELECT word FROM word WHERE word = any(%s) - and class = 'place' and type = 'postcode'""", - (plist,)) + cur.execute("SELECT word FROM word WHERE type = 'P' and word = any(%s)", + (plist,)) found = [row['word'] for row in cur] assert len(found) == len(set(found)), f"Duplicate rows for postcodes: {found}" diff --git a/test/php/Nominatim/AddressDetailsTest.php b/test/php/Nominatim/AddressDetailsTest.php deleted file mode 100644 index 2041dcb4..00000000 --- a/test/php/Nominatim/AddressDetailsTest.php +++ /dev/null @@ -1,118 +0,0 @@ -oDbStub = $this->getMockBuilder(\DB::class) - ->setMethods(array('getAll')) - ->getMock(); - $this->oDbStub->method('getAll') - ->willReturn($data); - } - - public function testGetLocaleAddress() - { - $oAD = new AddressDetails($this->oDbStub, 194663412, 10, 'en'); - $expected = join(', ', array( - '10 Downing Street', - '10', - 'Downing Street', - 'St. James\'s', - 'Covent Garden', - 'Westminster', - 'London', - 'Greater London', - 'England', - 'SW1A 2AA', - 'United Kingdom' - )); - $this->assertEquals($expected, $oAD->getLocaleAddress()); - } - - public function testGetAddressDetails() - { - $oAD = new AddressDetails($this->oDbStub, 194663412, 10, 'en'); - $this->assertEquals(18, count($oAD->getAddressDetails(true))); - $this->assertEquals(12, count($oAD->getAddressDetails(false))); - } - - public function testGetAddressNames() - { - $oAD = new AddressDetails($this->oDbStub, 194663412, 10, 'en'); - $expected = array( - 'tourism' => '10 Downing Street', - 'house_number' => '10', - 'road' => 'Downing Street', - 'neighbourhood' => 'St. James\'s', - 'suburb' => 'Covent Garden', - 'city' => 'London', - 'state_district' => 'Greater London', - 'state' => 'England', - 'ISO3166-2-lvl4' => 'GB-ENG', - 'ISO3166-2-lvl6' => 'GB-LND', - 'postcode' => 'SW1A 2AA', - 'country' => 'United Kingdom', - 'country_code' => 'gb' - ); - - $this->assertEquals($expected, $oAD->getAddressNames()); - } - - public function testGetAdminLevels() - { - $oAD = new AddressDetails($this->oDbStub, 194663412, 10, 'en'); - $expected = array( - 'level8' => 'Westminster', - 'level6' => 'London', - 'level5' => 'Greater London', - 'level4' => 'England', - 'level2' => 'United Kingdom' - ); - $this->assertEquals($expected, $oAD->getAdminLevels()); - } - - public function testDebugInfo() - { - $oAD = new AddressDetails($this->oDbStub, 194663412, 10, 'en'); - $this->assertTrue(is_array($oAD->debugInfo())); - $this->assertEquals(18, count($oAD->debugInfo())); - } -} diff --git a/test/php/Nominatim/ClassTypesTest.php b/test/php/Nominatim/ClassTypesTest.php deleted file mode 100644 index d2900d82..00000000 --- a/test/php/Nominatim/ClassTypesTest.php +++ /dev/null @@ -1,102 +0,0 @@ - 'boundary', 'type' => 'administrative', - 'rank_address' => '4', 'place_type' => 'city'); - $this->assertEquals('city', ClassTypes\getLabelTag($aPlace)); - - $aPlace = array('class' => 'boundary', 'type' => 'administrative', - 'rank_address' => '10'); - $this->assertEquals('state_district', ClassTypes\getLabelTag($aPlace)); - - $aPlace = array('class' => 'boundary', 'type' => 'administrative'); - $this->assertEquals('administrative', ClassTypes\getLabelTag($aPlace)); - - $aPlace = array('class' => 'place', 'type' => 'hamlet', 'rank_address' => '20'); - $this->assertEquals('hamlet', ClassTypes\getLabelTag($aPlace)); - - $aPlace = array('class' => 'highway', 'type' => 'residential', - 'rank_address' => '26'); - $this->assertEquals('road', ClassTypes\getLabelTag($aPlace)); - - $aPlace = array('class' => 'place', 'type' => 'house_number', - 'rank_address' => '30'); - $this->assertEquals('house_number', ClassTypes\getLabelTag($aPlace)); - - $aPlace = array('class' => 'amenity', 'type' => 'prison', - 'rank_address' => '30'); - $this->assertEquals('amenity', ClassTypes\getLabelTag($aPlace)); - } - - public function testGetLabel() - { - $aPlace = array('class' => 'boundary', 'type' => 'administrative', - 'rank_address' => '4', 'place_type' => 'city'); - $this->assertEquals('City', ClassTypes\getLabel($aPlace)); - - $aPlace = array('class' => 'boundary', 'type' => 'administrative', - 'rank_address' => '10'); - $this->assertEquals('State District', ClassTypes\getLabel($aPlace)); - - $aPlace = array('class' => 'boundary', 'type' => 'administrative'); - $this->assertEquals('Administrative', ClassTypes\getLabel($aPlace)); - - $aPlace = array('class' => 'amenity', 'type' => 'prison'); - $this->assertEquals('Prison', ClassTypes\getLabel($aPlace)); - - $aPlace = array('class' => 'amenity', 'type' => 'foobar'); - $this->assertNull(ClassTypes\getLabel($aPlace)); - } - - public function testGetBoundaryLabel() - { - $this->assertEquals('City', ClassTypes\getBoundaryLabel(8, null)); - $this->assertEquals('Administrative', ClassTypes\getBoundaryLabel(18, null)); - $this->assertEquals('None', ClassTypes\getBoundaryLabel(18, null, 'None')); - $this->assertEquals('State', ClassTypes\getBoundaryLabel(4, 'de', 'None')); - $this->assertEquals('County', ClassTypes\getBoundaryLabel(4, 'se', 'None')); - $this->assertEquals('Municipality', ClassTypes\getBoundaryLabel(7, 'se', 'None')); - } - - public function testGetDefRadius() - { - $aResult = array('class' => '', 'type' => ''); - $this->assertEquals(0.00005, ClassTypes\getDefRadius($aResult)); - - $aResult = array('class' => 'place', 'type' => 'country'); - $this->assertEquals(7, ClassTypes\getDefRadius($aResult)); - } - - public function testGetIcon() - { - $aResult = array('class' => '', 'type' => ''); - $this->assertNull(ClassTypes\getIcon($aResult)); - - $aResult = array('class' => 'place', 'type' => 'airport'); - $this->assertEquals('transport_airport2', ClassTypes\getIcon($aResult)); - } - - public function testGetImportance() - { - $aResult = array('class' => '', 'type' => ''); - $this->assertNull(ClassTypes\getImportance($aResult)); - - $aResult = array('class' => 'place', 'type' => 'airport'); - $this->assertGreaterThan(0, ClassTypes\getImportance($aResult)); - } -} diff --git a/test/php/Nominatim/DBTest.php b/test/php/Nominatim/DBTest.php deleted file mode 100644 index 1c6f7637..00000000 --- a/test/php/Nominatim/DBTest.php +++ /dev/null @@ -1,228 +0,0 @@ -connection = $oConnection; - } -} - -// phpcs:ignore PSR1.Classes.ClassDeclaration.MultipleClasses -class DBTest extends \PHPUnit\Framework\TestCase -{ - public function testReusingConnection() - { - $oDB = new NominatimSubClassedDB(''); - $oDB->setConnection('anything'); - $this->assertTrue($oDB->connect()); - } - - public function testCheckConnection() - { - $oDB = new \Nominatim\DB(''); - $this->assertFalse($oDB->checkConnection()); - } - - public function testErrorHandling() - { - $this->expectException(DatabaseError::class); - $this->expectExceptionMessage('Failed to establish database connection'); - - $oDB = new \Nominatim\DB('pgsql:dbname=abc'); - $oDB->connect(); - } - - public function testErrorHandling2() - { - $this->expectException(DatabaseError::class); - $this->expectExceptionMessage('Database query failed'); - - $oPDOStub = $this->getMockBuilder(PDO::class) - ->setMethods(array('query', 'quote')) - ->getMock(); - - $oPDOStub->method('query') - ->will($this->returnCallback(function ($sVal) { - return "'$sVal'"; - })); - - $oPDOStub->method('query') - ->will($this->returnCallback(function () { - throw new \PDOException('ERROR: syntax error at or near "FROM"'); - })); - - $oDB = new NominatimSubClassedDB(''); - $oDB->setConnection($oPDOStub); - $oDB->getOne('SELECT name FROM'); - } - - public function testGetPostgresVersion() - { - $oDBStub = $this->getMockBuilder(\Nominatim\DB::class) - ->disableOriginalConstructor() - ->setMethods(array('getOne')) - ->getMock(); - - $oDBStub->method('getOne') - ->willReturn('100006'); - - $this->assertEquals(10, $oDBStub->getPostgresVersion()); - } - - public function testGetPostgisVersion() - { - $oDBStub = $this->getMockBuilder(\Nominatim\DB::class) - ->disableOriginalConstructor() - ->setMethods(array('getOne')) - ->getMock(); - - $oDBStub->method('getOne') - ->willReturn('2.4.4'); - - $this->assertEquals(2.4, $oDBStub->getPostgisVersion()); - } - - public function testParseDSN() - { - $this->assertEquals( - array(), - \Nominatim\DB::parseDSN('') - ); - $this->assertEquals( - array( - 'database' => 'db1', - 'hostspec' => 'machine1' - ), - \Nominatim\DB::parseDSN('pgsql:dbname=db1;host=machine1') - ); - $this->assertEquals( - array( - 'database' => 'db1', - 'hostspec' => 'machine1', - 'port' => '1234', - 'username' => 'john', - 'password' => 'secret' - ), - \Nominatim\DB::parseDSN('pgsql:dbname=db1;host=machine1;port=1234;user=john;password=secret') - ); - } - - public function testGenerateDSN() - { - $this->assertEquals( - 'pgsql:', - \Nominatim\DB::generateDSN(array()) - ); - $this->assertEquals( - 'pgsql:host=machine1;dbname=db1', - \Nominatim\DB::generateDSN(\Nominatim\DB::parseDSN('pgsql:host=machine1;dbname=db1')) - ); - } - - public function testAgainstDatabase() - { - $unit_test_dsn = getenv('UNIT_TEST_DSN') != false ? - getenv('UNIT_TEST_DSN') : - 'pgsql:dbname=nominatim_unit_tests'; - - ## Create the database. - { - $aDSNParsed = \Nominatim\DB::parseDSN($unit_test_dsn); - $sDbname = $aDSNParsed['database']; - $aDSNParsed['database'] = 'postgres'; - - $oDB = new \Nominatim\DB(\Nominatim\DB::generateDSN($aDSNParsed)); - $oDB->connect(); - $oDB->exec('DROP DATABASE IF EXISTS ' . $sDbname); - $oDB->exec('CREATE DATABASE ' . $sDbname); - } - - $oDB = new \Nominatim\DB($unit_test_dsn); - $oDB->connect(); - - $this->assertTrue( - $oDB->checkConnection($sDbname) - ); - - # Tables, Indices - { - $oDB->exec('CREATE TABLE table1 (id integer, city varchar, country varchar)'); - - $this->assertTrue($oDB->tableExists('table1')); - $this->assertFalse($oDB->tableExists('table99')); - $this->assertFalse($oDB->tableExists(null)); - } - - # select queries - { - $oDB->exec( - "INSERT INTO table1 VALUES (1, 'Berlin', 'Germany'), (2, 'Paris', 'France')" - ); - - $this->assertEquals( - array( - array('city' => 'Berlin'), - array('city' => 'Paris') - ), - $oDB->getAll('SELECT city FROM table1') - ); - $this->assertEquals( - array(), - $oDB->getAll('SELECT city FROM table1 WHERE id=999') - ); - - - $this->assertEquals( - array('id' => 1, 'city' => 'Berlin', 'country' => 'Germany'), - $oDB->getRow('SELECT * FROM table1 WHERE id=1') - ); - $this->assertEquals( - false, - $oDB->getRow('SELECT * FROM table1 WHERE id=999') - ); - - - $this->assertEquals( - array('Berlin', 'Paris'), - $oDB->getCol('SELECT city FROM table1') - ); - $this->assertEquals( - array(), - $oDB->getCol('SELECT city FROM table1 WHERE id=999') - ); - - $this->assertEquals( - 'Berlin', - $oDB->getOne('SELECT city FROM table1 WHERE id=1') - ); - $this->assertEquals( - null, - $oDB->getOne('SELECT city FROM table1 WHERE id=999') - ); - - $this->assertEquals( - array('Berlin' => 'Germany', 'Paris' => 'France'), - $oDB->getAssoc('SELECT city, country FROM table1') - ); - $this->assertEquals( - array(), - $oDB->getAssoc('SELECT city, country FROM table1 WHERE id=999') - ); - } - } -} diff --git a/test/php/Nominatim/DatabaseErrorTest.php b/test/php/Nominatim/DatabaseErrorTest.php deleted file mode 100644 index e24049ca..00000000 --- a/test/php/Nominatim/DatabaseErrorTest.php +++ /dev/null @@ -1,39 +0,0 @@ -getMockBuilder(PDOException::class) - ->setMethods(array('getMessage')) - ->getMock(); - - $oSqlStub->method('getMessage') - ->willReturn('Unknown table.'); - - $oErr = new DatabaseError('Sql error', 123, null, $oSqlStub); - $this->assertEquals('Sql error', $oErr->getMessage()); - $this->assertEquals(123, $oErr->getCode()); - $this->assertEquals('Unknown table.', $oErr->getSqlError()); - } - - public function testSqlObjectDump() - { - $oErr = new DatabaseError('Sql error', 123, null, array('one' => 'two')); - $this->assertStringContainsString('two', $oErr->getSqlDebugDump()); - } -} diff --git a/test/php/Nominatim/DebugTest.php b/test/php/Nominatim/DebugTest.php deleted file mode 100644 index 84e8f215..00000000 --- a/test/php/Nominatim/DebugTest.php +++ /dev/null @@ -1,209 +0,0 @@ -oWithDebuginfo = $this->getMockBuilder(\GeococdeMock::class) - ->setMethods(array('debugInfo')) - ->getMock(); - $this->oWithDebuginfo->method('debugInfo') - ->willReturn(array('key1' => 'val1', 'key2' => 'val2', 'key3' => 'val3')); - - - $this->oWithToString = $this->getMockBuilder(\SomeMock::class) - ->setMethods(array('__toString')) - ->getMock(); - $this->oWithToString->method('__toString')->willReturn('me as string'); - } - - public function testPrintVar() - { - $this->expectOutputString(<<Var0: -
Var1:  True
-
Var2:  False
-
Var3:  0
-
Var4:  'String'
-
Var5:  0 => 'one'
-       1 => 'two'
-       2 => 'three'
-
Var6:  'key' => 'value'
-       'key2' => 'value2'
-
Var7:  me as string
-
Var8:  'value', 'value2'
- -EOT - ); - - Debug::printVar('Var0', null); - Debug::printVar('Var1', true); - Debug::printVar('Var2', false); - Debug::printVar('Var3', 0); - Debug::printVar('Var4', 'String'); - Debug::printVar('Var5', array('one', 'two', 'three')); - Debug::printVar('Var6', array('key' => 'value', 'key2' => 'value2')); - Debug::printVar('Var7', $this->oWithToString); - Debug::printVar('Var8', Debug::fmtArrayVals(array('key' => 'value', 'key2' => 'value2'))); - } - - - public function testDebugArray() - { - $this->expectOutputString(<<Arr0: 'null' -
Arr1:  'key1' => 'val1'
-       'key2' => 'val2'
-       'key3' => 'val3'
- -EOT - ); - - Debug::printDebugArray('Arr0', null); - Debug::printDebugArray('Arr1', $this->oWithDebuginfo); - } - - - public function testPrintDebugTable() - { - $this->expectOutputString(<<Table1: - -
-Table2: - -
-Table3: - - - - - - - - - - - - - -
01
'one'
'two'
'three'
'four'
-Table4: - - - - - - - - - - - -
key1key2key3
'val1'
'val2'
'val3'
- -EOT - ); - - Debug::printDebugTable('Table1', null); - - Debug::printDebugTable('Table2', array()); - - // Numeric headers - Debug::printDebugTable('Table3', array(array('one', 'two'), array('three', 'four'))); - - // Associate array - Debug::printDebugTable('Table4', array($this->oWithDebuginfo)); - } - - public function testPrintGroupTable() - { - $this->expectOutputString(<<Table1: - -
-Table2: - -
-Table3: - - - - - - - - - - - - - - - - - - - - - -
Groupkey1key2
group1
'val1'
'val2'
group1
'one'
'two'
group2
'val1'
'val2'
-Table4: - - - - - - - - - - - - - - - - - - - -
Groupkey1key2key3
group1
'val1'
'val2'
'val3'
group1
'val1'
'val2'
'val3'
- -EOT - ); - - Debug::printGroupTable('Table1', null); - Debug::printGroupTable('Table2', array()); - - // header are taken from first group item, thus no key3 gets printed - $aGroups = array( - 'group1' => array( - array('key1' => 'val1', 'key2' => 'val2'), - array('key1' => 'one', 'key2' => 'two', 'unknown' => 1), - ), - 'group2' => array( - array('key1' => 'val1', 'key2' => 'val2', 'key3' => 'val3'), - ) - ); - Debug::printGroupTable('Table3', $aGroups); - - $aGroups = array( - 'group1' => array($this->oWithDebuginfo, $this->oWithDebuginfo), - ); - Debug::printGroupTable('Table4', $aGroups); - } -} diff --git a/test/php/Nominatim/LibTest.php b/test/php/Nominatim/LibTest.php deleted file mode 100644 index 5d711240..00000000 --- a/test/php/Nominatim/LibTest.php +++ /dev/null @@ -1,94 +0,0 @@ -assertSame("'St. John's'", addQuotes("St. John's")); - $this->assertSame("''", addQuotes('')); - } - - public function testParseLatLon() - { - // no coordinates expected - $this->assertFalse(parseLatLon('')); - $this->assertFalse(parseLatLon('abc')); - $this->assertFalse(parseLatLon('12 34')); - - // coordinates expected - $this->assertNotNull(parseLatLon('0.0 -0.0')); - - $aRes = parseLatLon(' abc 12.456 -78.90 def '); - $this->assertEquals($aRes[1], 12.456); - $this->assertEquals($aRes[2], -78.90); - $this->assertEquals($aRes[0], ' 12.456 -78.90 '); - - $aRes = parseLatLon(' [12.456,-78.90] '); - $this->assertEquals($aRes[1], 12.456); - $this->assertEquals($aRes[2], -78.90); - $this->assertEquals($aRes[0], ' [12.456,-78.90] '); - - $aRes = parseLatLon(' -12.456,-78.90 '); - $this->assertEquals($aRes[1], -12.456); - $this->assertEquals($aRes[2], -78.90); - $this->assertEquals($aRes[0], ' -12.456,-78.90 '); - - // http://en.wikipedia.org/wiki/Geographic_coordinate_conversion - // these all represent the same location - $aQueries = array( - '40 26.767 N 79 58.933 W', - '40° 26.767′ N 79° 58.933′ W', - "40° 26.767' N 79° 58.933' W", - "40° 26.767' - N 79° 58.933' W", - 'N 40 26.767, W 79 58.933', - 'N 40°26.767′, W 79°58.933′', - ' N 40°26.767′, W 79°58.933′', - "N 40°26.767', W 79°58.933'", - - '40 26 46 N 79 58 56 W', - '40° 26′ 46″ N 79° 58′ 56″ W', - '40° 26′ 46.00″ N 79° 58′ 56.00″ W', - '40°26′46″N 79°58′56″W', - 'N 40 26 46 W 79 58 56', - 'N 40° 26′ 46″, W 79° 58′ 56″', - 'N 40° 26\' 46", W 79° 58\' 56"', - 'N 40° 26\' 46", W 79° 58\' 56"', - - '40.446 -79.982', - '40.446,-79.982', - '40.446° N 79.982° W', - 'N 40.446° W 79.982°', - - '[40.446 -79.982]', - '[40.446, -79.982]', - ' 40.446 , -79.982 ', - ' 40.446 , -79.982 ', - ' 40.446 , -79.982 ', - ' 40.446 , -79.982 ', - ); - - - foreach ($aQueries as $sQuery) { - $aRes = parseLatLon($sQuery); - $this->assertEqualsWithDelta(40.446, $aRes[1], 0.01, 'degrees decimal ' . $sQuery); - $this->assertEqualsWithDelta(-79.982, $aRes[2], 0.01, 'degrees decimal ' . $sQuery); - $this->assertEquals($sQuery, $aRes[0]); - } - } -} diff --git a/test/php/Nominatim/ParameterParserTest.php b/test/php/Nominatim/ParameterParserTest.php deleted file mode 100644 index 82716d4d..00000000 --- a/test/php/Nominatim/ParameterParserTest.php +++ /dev/null @@ -1,248 +0,0 @@ - '1', - 'bool2' => '0', - 'bool3' => 'true', - 'bool4' => 'false', - 'bool5' => '' - )); - - $this->assertSame(false, $oParams->getBool('non-exists')); - $this->assertSame(true, $oParams->getBool('non-exists', true)); - $this->assertSame(true, $oParams->getBool('bool1')); - $this->assertSame(false, $oParams->getBool('bool2')); - $this->assertSame(true, $oParams->getBool('bool3')); - $this->assertSame(true, $oParams->getBool('bool4')); - $this->assertSame(false, $oParams->getBool('bool5')); - } - - - public function testGetInt() - { - $oParams = new ParameterParser(array( - 'int1' => '5', - 'int2' => '-1', - 'int3' => 0 - )); - - $this->assertSame(false, $oParams->getInt('non-exists')); - $this->assertSame(999, $oParams->getInt('non-exists', 999)); - $this->assertSame(5, $oParams->getInt('int1')); - - $this->assertSame(-1, $oParams->getInt('int2')); - $this->assertSame(0, $oParams->getInt('int3')); - } - - - public function testGetIntWithNonNumber() - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage("Integer number expected for parameter 'int4'"); - - (new ParameterParser(array('int4' => 'a')))->getInt('int4'); - } - - - public function testGetIntWithEmpytString() - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage("Integer number expected for parameter 'int5'"); - - (new ParameterParser(array('int5' => '')))->getInt('int5'); - } - - - public function testGetFloat() - { - - $oParams = new ParameterParser(array( - 'float1' => '1.0', - 'float2' => '-5', - 'float3' => 0 - )); - - $this->assertSame(false, $oParams->getFloat('non-exists')); - $this->assertSame(999, $oParams->getFloat('non-exists', 999)); - $this->assertSame(1.0, $oParams->getFloat('float1')); - $this->assertSame(-5.0, $oParams->getFloat('float2')); - $this->assertSame(0.0, $oParams->getFloat('float3')); - } - - public function testGetFloatWithEmptyString() - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage("Floating-point number expected for parameter 'float4'"); - - (new ParameterParser(array('float4' => '')))->getFloat('float4'); - } - - public function testGetFloatWithTextString() - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage("Floating-point number expected for parameter 'float5'"); - - (new ParameterParser(array('float5' => 'a')))->getFloat('float5'); - } - - - public function testGetFloatWithInvalidNumber() - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage("Floating-point number expected for parameter 'float6'"); - - (new ParameterParser(array('float6' => '-55.')))->getFloat('float6'); - } - - - public function testGetString() - { - $oParams = new ParameterParser(array( - 'str1' => 'abc', - 'str2' => '', - 'str3' => '0' - )); - - $this->assertSame(false, $oParams->getString('non-exists')); - $this->assertSame('default', $oParams->getString('non-exists', 'default')); - $this->assertSame('abc', $oParams->getString('str1')); - $this->assertSame(false, $oParams->getStringList('str2')); - $this->assertSame(false, $oParams->getStringList('str3')); // sadly PHP magic treats 0 as false when returned - } - - - public function testGetSet() - { - $oParams = new ParameterParser(array( - 'val1' => 'foo', - 'val2' => '', - 'val3' => 0 - )); - - $this->assertSame(false, $oParams->getSet('non-exists', array('foo', 'bar'))); - $this->assertSame('default', $oParams->getSet('non-exists', array('foo', 'bar'), 'default')); - $this->assertSame('foo', $oParams->getSet('val1', array('foo', 'bar'))); - - $this->assertSame(false, $oParams->getSet('val2', array('foo', 'bar'))); - $this->assertSame(false, $oParams->getSet('val3', array('foo', 'bar'))); - } - - - public function testGetSetWithValueNotInSet() - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage("Parameter 'val4' must be one of: foo, bar"); - - (new ParameterParser(array('val4' => 'faz')))->getSet('val4', array('foo', 'bar')); - } - - - public function testGetStringList() - { - $oParams = new ParameterParser(array( - 'list1' => ',a,b,c,,c,d', - 'list2' => 'a', - 'list3' => '', - 'list4' => '0' - )); - - $this->assertSame(false, $oParams->getStringList('non-exists')); - $this->assertSame(array('a', 'b'), $oParams->getStringList('non-exists', array('a', 'b'))); - $this->assertSame(array('a', 'b', 'c', 'c', 'd'), $oParams->getStringList('list1')); - $this->assertSame(array('a'), $oParams->getStringList('list2')); - $this->assertSame(false, $oParams->getStringList('list3')); - $this->assertSame(false, $oParams->getStringList('list4')); - } - - - public function testGetPreferredLanguages() - { - $oParams = new ParameterParser(array('accept-language' => '')); - $this->assertSame(array( - 'name:default' => 'name:default', - '_place_name:default' => '_place_name:default', - 'name' => 'name', - '_place_name' => '_place_name' - ), array_slice($oParams->getPreferredLanguages('default'), 0, 4)); - - $oParams = new ParameterParser(array('accept-language' => 'de,en')); - $this->assertSame(array( - 'name:de' => 'name:de', - '_place_name:de' => '_place_name:de', - 'name:en' => 'name:en', - '_place_name:en' => '_place_name:en', - 'name' => 'name', - '_place_name' => '_place_name' - ), array_slice($oParams->getPreferredLanguages('default'), 0, 6)); - - $oParams = new ParameterParser(array('accept-language' => 'fr-ca,fr;q=0.8,en-ca;q=0.5,en;q=0.3')); - $this->assertSame(array( - 'name:fr-ca' => 'name:fr-ca', - '_place_name:fr-ca' => '_place_name:fr-ca', - 'name:fr' => 'name:fr', - '_place_name:fr' => '_place_name:fr', - 'name:en-ca' => 'name:en-ca', - '_place_name:en-ca' => '_place_name:en-ca', - 'name:en' => 'name:en', - '_place_name:en' => '_place_name:en', - 'name' => 'name', - '_place_name' => '_place_name' - ), array_slice($oParams->getPreferredLanguages('default'), 0, 10)); - - $oParams = new ParameterParser(array('accept-language' => 'ja_rm,zh_pinyin')); - $this->assertSame(array( - 'name:ja_rm' => 'name:ja_rm', - '_place_name:ja_rm' => '_place_name:ja_rm', - 'name:zh_pinyin' => 'name:zh_pinyin', - '_place_name:zh_pinyin' => '_place_name:zh_pinyin', - 'name:ja' => 'name:ja', - '_place_name:ja' => '_place_name:ja', - 'name:zh' => 'name:zh', - '_place_name:zh' => '_place_name:zh', - 'name' => 'name', - '_place_name' => '_place_name' - ), array_slice($oParams->getPreferredLanguages('default'), 0, 10)); - } - - public function testHasSetAny() - { - $oParams = new ParameterParser(array( - 'one' => '', - 'two' => 0, - 'three' => '0', - 'four' => '1', - 'five' => 'anystring' - )); - $this->assertFalse($oParams->hasSetAny(array())); - $this->assertFalse($oParams->hasSetAny(array(''))); - $this->assertFalse($oParams->hasSetAny(array('unknown'))); - $this->assertFalse($oParams->hasSetAny(array('one', 'two', 'three'))); - $this->assertTrue($oParams->hasSetAny(array('one', 'four'))); - $this->assertTrue($oParams->hasSetAny(array('four'))); - $this->assertTrue($oParams->hasSetAny(array('five'))); - } -} diff --git a/test/php/Nominatim/ResultTest.php b/test/php/Nominatim/ResultTest.php deleted file mode 100644 index 8b95105e..00000000 --- a/test/php/Nominatim/ResultTest.php +++ /dev/null @@ -1,43 +0,0 @@ -iResultRank = $iResultRank; - - return $oResult; -} - - -class ResultTest extends \PHPUnit\Framework\TestCase -{ - public function testSplitResults() - { - $aSplitResults = Result::splitResults(array( - mkRankedResult(1, 2), - mkRankedResult(2, 0), - mkRankedResult(3, 0), - mkRankedResult(4, 2), - mkRankedResult(5, 1) - )); - - - $aHead = array_keys($aSplitResults['head']); - $aTail = array_keys($aSplitResults['tail']); - - $this->assertEquals($aHead, array(2, 3)); - $this->assertEquals($aTail, array(1, 4, 5)); - } -} diff --git a/test/php/Nominatim/SearchContextTest.php b/test/php/Nominatim/SearchContextTest.php deleted file mode 100644 index b5ef1a7a..00000000 --- a/test/php/Nominatim/SearchContextTest.php +++ /dev/null @@ -1,89 +0,0 @@ -oCtx = new SearchContext(); - } - - public function testHasNearPoint() - { - $this->assertFalse($this->oCtx->hasNearPoint()); - $this->oCtx->setNearPoint(0, 0); - $this->assertTrue($this->oCtx->hasNearPoint()); - } - - public function testNearRadius() - { - $this->oCtx->setNearPoint(1, 1); - $this->assertEquals(0.1, $this->oCtx->nearRadius()); - $this->oCtx->setNearPoint(1, 1, 0.338); - $this->assertEquals(0.338, $this->oCtx->nearRadius()); - } - - public function testWithinSQL() - { - $this->oCtx->setNearPoint(0.1, 23, 1); - - $this->assertEquals( - 'ST_DWithin(foo, ST_SetSRID(ST_Point(23,0.1),4326), 1.000000)', - $this->oCtx->withinSQL('foo') - ); - } - - public function testDistanceSQL() - { - $this->oCtx->setNearPoint(0.1, 23, 1); - - $this->assertEquals( - 'ST_Distance(ST_SetSRID(ST_Point(23,0.1),4326), foo)', - $this->oCtx->distanceSQL('foo') - ); - } - - public function testSetViewboxFromBox() - { - $viewbox = array(30, 20, 40, 50); - $this->oCtx->setViewboxFromBox($viewbox, true); - $this->assertEquals( - 'ST_SetSRID(ST_MakeBox2D(ST_Point(30.000000,20.000000),ST_Point(40.000000,50.000000)),4326)', - $this->oCtx->sqlViewboxSmall - ); - // height: 10 - // width: 30 - $this->assertEquals( - 'ST_SetSRID(ST_MakeBox2D(ST_Point(50.000000,80.000000),ST_Point(20.000000,-10.000000)),4326)', - $this->oCtx->sqlViewboxLarge - ); - - - $viewbox = array(-1.5, -2, 1.5, 2); - $this->oCtx->setViewboxFromBox($viewbox, true); - $this->assertEquals( - 'ST_SetSRID(ST_MakeBox2D(ST_Point(-1.500000,-2.000000),ST_Point(1.500000,2.000000)),4326)', - $this->oCtx->sqlViewboxSmall - ); - // height: 3 - // width: 4 - $this->assertEquals( - 'ST_SetSRID(ST_MakeBox2D(ST_Point(4.500000,6.000000),ST_Point(-4.500000,-6.000000)),4326)', - $this->oCtx->sqlViewboxLarge - ); - } -} diff --git a/test/php/Nominatim/ShellTest.php b/test/php/Nominatim/ShellTest.php deleted file mode 100644 index 82219498..00000000 --- a/test/php/Nominatim/ShellTest.php +++ /dev/null @@ -1,128 +0,0 @@ -expectException('ArgumentCountError'); - $this->expectExceptionMessage('Too few arguments to function'); - $oCmd = new \Nominatim\Shell(); - - - $oCmd = new \Nominatim\Shell('wc', '-l', 'file.txt'); - $this->assertSame( - "wc -l 'file.txt'", - $oCmd->escapedCmd() - ); - } - - public function testaddParams() - { - $oCmd = new \Nominatim\Shell('grep'); - $oCmd->addParams('-a', 'abc') - ->addParams(10); - - $this->assertSame( - 'grep -a abc 10', - $oCmd->escapedCmd(), - 'no escaping needed, chained' - ); - - $oCmd = new \Nominatim\Shell('grep'); - $oCmd->addParams(); - $oCmd->addParams(null); - $oCmd->addParams(''); - - $this->assertEmpty($oCmd->aParams); - $this->assertSame('grep', $oCmd->escapedCmd(), 'empty params'); - - $oCmd = new \Nominatim\Shell('echo', '-n', 0); - $this->assertSame( - 'echo -n 0', - $oCmd->escapedCmd(), - 'zero param' - ); - - $oCmd = new \Nominatim\Shell('/path with space/do.php'); - $oCmd->addParams('-a', ' b '); - $oCmd->addParams('--flag'); - $oCmd->addParams('two words'); - $oCmd->addParams('v=1'); - - $this->assertSame( - "'/path with space/do.php' -a ' b ' --flag 'two words' 'v=1'", - $oCmd->escapedCmd(), - 'escape whitespace' - ); - - $oCmd = new \Nominatim\Shell('grep'); - $oCmd->addParams(';', '|more&', '2>&1'); - - $this->assertSame( - "grep ';' '|more&' '2>&1'", - $oCmd->escapedCmd(), - 'escape shell characters' - ); - } - - public function testaddEnvPair() - { - $oCmd = new \Nominatim\Shell('date'); - - $oCmd->addEnvPair('one', 'two words') - ->addEnvPair('null', null) - ->addEnvPair(null, 'null') - ->addEnvPair('empty', '') - ->addEnvPair('', 'empty'); - - $this->assertEquals( - array('one' => 'two words', 'empty' => ''), - $oCmd->aEnv - ); - - $oCmd->addEnvPair('one', 'overwrite'); - $this->assertEquals( - array('one' => 'overwrite', 'empty' => ''), - $oCmd->aEnv - ); - } - - public function testClone() - { - $oCmd = new \Nominatim\Shell('wc', '-l', 'file.txt'); - $oCmd2 = clone $oCmd; - $oCmd->addParams('--flag'); - $oCmd2->addParams('--flag2'); - - $this->assertSame( - "wc -l 'file.txt' --flag", - $oCmd->escapedCmd() - ); - - $this->assertSame( - "wc -l 'file.txt' --flag2", - $oCmd2->escapedCmd() - ); - } - - public function testRun() - { - $oCmd = new \Nominatim\Shell('echo'); - - $this->assertSame(0, $oCmd->run()); - - // var_dump($sStdout); - } -} diff --git a/test/php/Nominatim/SimpleWordListTest.php b/test/php/Nominatim/SimpleWordListTest.php deleted file mode 100644 index 69cb5180..00000000 --- a/test/php/Nominatim/SimpleWordListTest.php +++ /dev/null @@ -1,136 +0,0 @@ -aTokens = array_flip($aTokens); - } - - public function containsAny($sTerm) - { - return isset($this->aTokens[$sTerm]); - } -} - -// phpcs:ignore PSR1.Classes.ClassDeclaration.MultipleClasses -class SimpleWordListTest extends \PHPUnit\Framework\TestCase -{ - - - private function serializeSets($aSets) - { - $aParts = array(); - foreach ($aSets as $aSet) { - $aParts[] = '(' . join('|', $aSet) . ')'; - } - return join(',', $aParts); - } - - - public function testEmptyPhrase() - { - $oList = new SimpleWordList(''); - $this->assertNull($oList->getWordSets(new TokensFullSet())); - } - - - public function testSingleWordPhrase() - { - $oList = new SimpleWordList('a'); - - $this->assertEquals( - '(a)', - $this->serializeSets($oList->getWordSets(new TokensFullSet())) - ); - } - - - public function testMultiWordPhrase() - { - $oList = new SimpleWordList('a b'); - $this->assertEquals( - '(a b),(a|b)', - $this->serializeSets($oList->getWordSets(new TokensFullSet())) - ); - - $oList = new SimpleWordList('a b c'); - $this->assertEquals( - '(a b c),(a b|c),(a|b c),(a|b|c)', - $this->serializeSets($oList->getWordSets(new TokensFullSet())) - ); - - $oList = new SimpleWordList('a b c d'); - $this->assertEquals( - '(a b c d),(a b c|d),(a b|c d),(a|b c d),(a b|c|d),(a|b c|d),(a|b|c d),(a|b|c|d)', - $this->serializeSets($oList->getWordSets(new TokensFullSet())) - ); - } - - public function testCmpByArraylen() - { - // Array elements are phrases, we want to sort so longest phrases are first - $aList1 = array('hackney', 'bridge', 'london', 'england'); - $aList2 = array('hackney', 'london', 'bridge'); - $aList3 = array('bridge', 'hackney', 'london', 'england'); - - $this->assertEquals(0, \Nominatim\SimpleWordList::cmpByArraylen($aList1, $aList1)); - - // list2 "wins". Less array elements - $this->assertEquals(1, \Nominatim\SimpleWordList::cmpByArraylen($aList1, $aList2)); - $this->assertEquals(-1, \Nominatim\SimpleWordList::cmpByArraylen($aList2, $aList3)); - - // list1 "wins". Same number of array elements but longer first element - $this->assertEquals(-1, \Nominatim\SimpleWordList::cmpByArraylen($aList1, $aList3)); - } - - public function testMaxWordSets() - { - $aWords = array_fill(0, 4, 'a'); - $oList = new SimpleWordList(join(' ', $aWords)); - $this->assertEquals(8, count($oList->getWordSets(new TokensFullSet()))); - - $aWords = array_fill(0, 18, 'a'); - $oList = new SimpleWordList(join(' ', $aWords)); - $this->assertEquals(100, count($oList->getWordSets(new TokensFullSet()))); - } - - - public function testPartialTokensShortTerm() - { - $oList = new SimpleWordList('a b c d'); - $this->assertEquals( - '(a|b c d),(a|b c|d)', - $this->serializeSets($oList->getWordSets(new TokensPartialSet(array('a', 'b', 'd', 'b c', 'b c d')))) - ); - } - - - public function testPartialTokensLongTerm() - { - $aWords = array_fill(0, 18, 'a'); - $oList = new SimpleWordList(join(' ', $aWords)); - $this->assertEquals(80, count($oList->getWordSets(new TokensPartialSet(array('a', 'a a a a a'))))); - } -} diff --git a/test/php/Nominatim/StatusTest.php b/test/php/Nominatim/StatusTest.php deleted file mode 100644 index 5f8bac64..00000000 --- a/test/php/Nominatim/StatusTest.php +++ /dev/null @@ -1,81 +0,0 @@ -expectException(\Exception::class); - $this->expectExceptionMessage('No database'); - $this->expectExceptionCode(700); - - $oDB = null; - $oStatus = new Status($oDB); - $this->assertEquals('No database', $oStatus->status()); - } - - public function testNoDatabaseConnectionFail() - { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Database connection failed'); - $this->expectExceptionCode(700); - - $oDbStub = $this->getMockBuilder(Nominatim\DB::class) - ->setMethods(array('connect')) - ->getMock(); - - $oDbStub->method('connect') - ->will($this->returnCallback(function () { - throw new \Nominatim\DatabaseError('psql connection problem', 500, null, 'unknown database'); - })); - - - $oStatus = new Status($oDbStub); - $this->assertEquals('No database', $oStatus->status()); - } - - public function testOK() - { - $oDbStub = $this->getMockBuilder(Nominatim\DB::class) - ->setMethods(array('connect', 'getOne')) - ->getMock(); - - $oDbStub->method('getOne') - ->will($this->returnCallback(function ($sql) { - if (preg_match("/make_standard_name\('(\w+)'\)/", $sql, $aMatch)) return $aMatch[1]; - if (preg_match('/SELECT word_id, word_token/', $sql)) return 1234; - })); - - $oStatus = new Status($oDbStub); - $this->assertNull($oStatus->status()); - } - - public function testDataDate() - { - $oDbStub = $this->getMockBuilder(Nominatim\DB::class) - ->setMethods(array('getOne')) - ->getMock(); - - $oDbStub->method('getOne') - ->willReturn(1519430221); - - $oStatus = new Status($oDbStub); - $this->assertEquals(1519430221, $oStatus->dataDate()); - } -} diff --git a/test/php/Nominatim/TokenListTest.php b/test/php/Nominatim/TokenListTest.php deleted file mode 100644 index 57e3c58f..00000000 --- a/test/php/Nominatim/TokenListTest.php +++ /dev/null @@ -1,60 +0,0 @@ -oNormalizer = $this->getMockBuilder(\MockNormalizer::class) - ->setMethods(array('transliterate')) - ->getMock(); - $this->oNormalizer->method('transliterate') - ->will($this->returnCallback(function ($text) { - return strtolower($text); - })); - } - - private function wordResult($aFields) - { - $aRow = array( - 'word_id' => null, - 'word_token' => null, - 'word' => null, - 'class' => null, - 'type' => null, - 'country_code' => null, - 'count' => 0 - ); - return array_merge($aRow, $aFields); - } - - public function testList() - { - $TL = new TokenList; - - $this->assertEquals(0, $TL->count()); - - $TL->addToken('word1', 'token1'); - $TL->addToken('word1', 'token2'); - - $this->assertEquals(1, $TL->count()); - - $this->assertTrue($TL->contains('word1')); - $this->assertEquals(array('token1', 'token2'), $TL->get('word1')); - - $this->assertFalse($TL->contains('unknownword')); - $this->assertEquals(array(), $TL->get('unknownword')); - } -} diff --git a/test/php/Nominatim/tokenizer.php b/test/php/Nominatim/tokenizer.php deleted file mode 100644 index 923e0a22..00000000 --- a/test/php/Nominatim/tokenizer.php +++ /dev/null @@ -1,25 +0,0 @@ -oDB =& $oDB; - } - - public function checkStatus() - { - } -} diff --git a/test/php/bootstrap.php b/test/php/bootstrap.php deleted file mode 100644 index 7d254511..00000000 --- a/test/php/bootstrap.php +++ /dev/null @@ -1,14 +0,0 @@ - - - - - ../../lib-php/ - - - - - - - ./Nominatim - - - diff --git a/test/python/api/conftest.py b/test/python/api/conftest.py index 0c770980..3ca0720b 100644 --- a/test/python/api/conftest.py +++ b/test/python/api/conftest.py @@ -7,7 +7,6 @@ """ Helper fixtures for API call tests. """ -from pathlib import Path import pytest import pytest_asyncio import time @@ -24,7 +23,7 @@ import nominatim_api.logging as loglib class APITester: def __init__(self): - self.api = napi.NominatimAPI(Path('/invalid')) + self.api = napi.NominatimAPI() self.async_to_sync(self.api._async_api.setup_database()) @@ -229,11 +228,9 @@ def frontend(request, event_loop, tmp_path): apiobj.async_to_sync(_do_sql()) - event_loop.run_until_complete(convert_sqlite.convert(Path('/invalid'), - db, options)) - outapi = napi.NominatimAPI(Path('/invalid'), - {'NOMINATIM_DATABASE_DSN': f"sqlite:dbname={db}", - 'NOMINATIM_USE_US_TIGER_DATA': 'yes'}) + event_loop.run_until_complete(convert_sqlite.convert(None, db, options)) + outapi = napi.NominatimAPI(environ={'NOMINATIM_DATABASE_DSN': f"sqlite:dbname={db}", + 'NOMINATIM_USE_US_TIGER_DATA': 'yes'}) testapis.append(outapi) return outapi @@ -249,5 +246,5 @@ def frontend(request, event_loop, tmp_path): @pytest_asyncio.fixture async def api(temp_db): - async with napi.NominatimAPIAsync(Path('/invalid')) as api: + async with napi.NominatimAPIAsync() as api: yield api diff --git a/test/python/api/search/test_api_search_query.py b/test/python/api/search/test_api_search_query.py index 7154ae08..71caf5b7 100644 --- a/test/python/api/search/test_api_search_query.py +++ b/test/python/api/search/test_api_search_query.py @@ -19,7 +19,7 @@ class MyToken(query.Token): def mktoken(tid: int): return MyToken(penalty=3.0, token=tid, count=1, addr_count=1, - lookup_word='foo', is_indexed=True) + lookup_word='foo') @pytest.mark.parametrize('ptype,ttype', [('NONE', 'WORD'), diff --git a/test/python/api/search/test_db_search_builder.py b/test/python/api/search/test_db_search_builder.py index 5d984014..371a6f02 100644 --- a/test/python/api/search/test_db_search_builder.py +++ b/test/python/api/search/test_db_search_builder.py @@ -33,7 +33,7 @@ def make_query(*args): q.add_token(TokenRange(start, end), ttype, MyToken(penalty=0.5 if ttype == TokenType.PARTIAL else 0.0, token=tid, count=1, addr_count=1, - lookup_word=word, is_indexed=True)) + lookup_word=word)) return q @@ -397,14 +397,14 @@ def make_counted_searches(name_part, name_full, address_part, address_full, q.add_node(BreakType.END, PhraseType.NONE) q.add_token(TokenRange(0, 1), TokenType.PARTIAL, - MyToken(0.5, 1, name_part, 1, 'name_part', True)) + MyToken(0.5, 1, name_part, 1, 'name_part')) q.add_token(TokenRange(0, 1), TokenType.WORD, - MyToken(0, 101, name_full, 1, 'name_full', True)) + MyToken(0, 101, name_full, 1, 'name_full')) for i in range(num_address_parts): q.add_token(TokenRange(i + 1, i + 2), TokenType.PARTIAL, - MyToken(0.5, 2, address_part, 1, 'address_part', True)) + MyToken(0.5, 2, address_part, 1, 'address_part')) q.add_token(TokenRange(i + 1, i + 2), TokenType.WORD, - MyToken(0, 102, address_full, 1, 'address_full', True)) + MyToken(0, 102, address_full, 1, 'address_full')) builder = SearchBuilder(q, SearchDetails()) diff --git a/test/python/api/search/test_icu_query_analyzer.py b/test/python/api/search/test_icu_query_analyzer.py index 7f88879c..ac4bcbb7 100644 --- a/test/python/api/search/test_icu_query_analyzer.py +++ b/test/python/api/search/test_icu_query_analyzer.py @@ -7,8 +7,6 @@ """ Tests for query analyzer for ICU tokenizer. """ -from pathlib import Path - import pytest import pytest_asyncio @@ -40,7 +38,7 @@ async def conn(table_factory): table_factory('word', definition='word_id INT, word_token TEXT, type TEXT, word TEXT, info JSONB') - async with NominatimAPIAsync(Path('/invalid'), {}) as api: + async with NominatimAPIAsync() as api: async with api.begin() as conn: yield conn diff --git a/test/python/api/search/test_legacy_query_analyzer.py b/test/python/api/search/test_legacy_query_analyzer.py deleted file mode 100644 index 0e967c10..00000000 --- a/test/python/api/search/test_legacy_query_analyzer.py +++ /dev/null @@ -1,243 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# -# This file is part of Nominatim. (https://nominatim.org) -# -# Copyright (C) 2024 by the Nominatim developer community. -# For a full list of authors see the git log. -""" -Tests for query analyzer for legacy tokenizer. -""" -from pathlib import Path - -import pytest -import pytest_asyncio - -from nominatim_api import NominatimAPIAsync -from nominatim_api.search.query import Phrase, PhraseType, TokenType, BreakType -import nominatim_api.search.legacy_tokenizer as tok -from nominatim_api.logging import set_log_output, get_and_disable - - -async def add_word(conn, word_id, word_token, word, count): - t = conn.t.meta.tables['word'] - await conn.execute(t.insert(), {'word_id': word_id, - 'word_token': word_token, - 'search_name_count': count, - 'word': word}) - - -async def add_housenumber(conn, word_id, hnr): - t = conn.t.meta.tables['word'] - await conn.execute(t.insert(), {'word_id': word_id, - 'word_token': ' ' + hnr, - 'word': hnr, - 'class': 'place', - 'type': 'house'}) - - -async def add_postcode(conn, word_id, postcode): - t = conn.t.meta.tables['word'] - await conn.execute(t.insert(), {'word_id': word_id, - 'word_token': ' ' + postcode, - 'word': postcode, - 'class': 'place', - 'type': 'postcode'}) - - -async def add_special_term(conn, word_id, word_token, cls, typ, op): - t = conn.t.meta.tables['word'] - await conn.execute(t.insert(), {'word_id': word_id, - 'word_token': word_token, - 'word': word_token, - 'class': cls, - 'type': typ, - 'operator': op}) - - -def make_phrase(query): - return [Phrase(PhraseType.NONE, s) for s in query.split(',')] - - -@pytest_asyncio.fixture -async def conn(table_factory, temp_db_cursor): - """ Create an asynchronous SQLAlchemy engine for the test DB. - """ - table_factory('nominatim_properties', - definition='property TEXT, value TEXT', - content=(('tokenizer_maxwordfreq', '10000'), )) - table_factory('word', - definition="""word_id INT, word_token TEXT, word TEXT, - class TEXT, type TEXT, country_code TEXT, - search_name_count INT, operator TEXT - """) - - temp_db_cursor.execute("""CREATE OR REPLACE FUNCTION make_standard_name(name TEXT) - RETURNS TEXT AS $$ SELECT lower(name); $$ LANGUAGE SQL;""") - - async with NominatimAPIAsync(Path('/invalid'), {}) as api: - async with api.begin() as conn: - yield conn - - -@pytest.mark.asyncio -async def test_empty_phrase(conn): - ana = await tok.create_query_analyzer(conn) - - query = await ana.analyze_query([]) - - assert len(query.source) == 0 - assert query.num_token_slots() == 0 - - -@pytest.mark.asyncio -async def test_single_phrase_with_unknown_terms(conn): - ana = await tok.create_query_analyzer(conn) - - await add_word(conn, 1, 'foo', 'FOO', 3) - - query = await ana.analyze_query(make_phrase('foo BAR')) - - assert len(query.source) == 1 - assert query.source[0].ptype == PhraseType.NONE - assert query.source[0].text == 'foo bar' - - assert query.num_token_slots() == 2 - assert len(query.nodes[0].starting) == 1 - assert not query.nodes[1].starting - - -@pytest.mark.asyncio -async def test_multiple_phrases(conn): - ana = await tok.create_query_analyzer(conn) - - await add_word(conn, 1, 'one', 'one', 13) - await add_word(conn, 2, 'two', 'two', 45) - await add_word(conn, 100, 'one two', 'one two', 3) - await add_word(conn, 3, 'three', 'three', 4584) - - query = await ana.analyze_query(make_phrase('one two,three')) - - assert len(query.source) == 2 - - -@pytest.mark.asyncio -async def test_housenumber_token(conn): - ana = await tok.create_query_analyzer(conn) - - await add_housenumber(conn, 556, '45 a') - - query = await ana.analyze_query(make_phrase('45 A')) - - assert query.num_token_slots() == 2 - assert len(query.nodes[0].starting) == 2 - - query.nodes[0].starting.sort(key=lambda tl: tl.end) - - hn1 = query.nodes[0].starting[0] - assert hn1.ttype == TokenType.HOUSENUMBER - assert hn1.end == 1 - assert hn1.tokens[0].token == 0 - - hn2 = query.nodes[0].starting[1] - assert hn2.ttype == TokenType.HOUSENUMBER - assert hn2.end == 2 - assert hn2.tokens[0].token == 556 - - -@pytest.mark.asyncio -async def test_postcode_token(conn): - ana = await tok.create_query_analyzer(conn) - - await add_postcode(conn, 34, '45ax') - - query = await ana.analyze_query(make_phrase('45AX')) - - assert query.num_token_slots() == 1 - assert [tl.ttype for tl in query.nodes[0].starting] == [TokenType.POSTCODE] - - -@pytest.mark.asyncio -async def test_partial_tokens(conn): - ana = await tok.create_query_analyzer(conn) - - await add_word(conn, 1, ' foo', 'foo', 99) - await add_word(conn, 1, 'foo', 'FOO', 99) - await add_word(conn, 1, 'bar', 'FOO', 990000) - - query = await ana.analyze_query(make_phrase('foo bar')) - - assert query.num_token_slots() == 2 - - first = query.nodes[0].starting - first.sort(key=lambda tl: tl.tokens[0].penalty) - assert [tl.ttype for tl in first] == [TokenType.WORD, TokenType.PARTIAL] - assert all(tl.tokens[0].lookup_word == 'foo' for tl in first) - - second = query.nodes[1].starting - assert [tl.ttype for tl in second] == [TokenType.PARTIAL] - assert not second[0].tokens[0].is_indexed - - -@pytest.mark.asyncio -@pytest.mark.parametrize('term,order', [('23456', ['POSTCODE', 'HOUSENUMBER', 'WORD', 'PARTIAL']), - ('3', ['HOUSENUMBER', 'POSTCODE', 'WORD', 'PARTIAL']) - ]) -async def test_penalty_postcodes_and_housenumbers(conn, term, order): - ana = await tok.create_query_analyzer(conn) - - await add_postcode(conn, 1, term) - await add_housenumber(conn, 2, term) - await add_word(conn, 3, term, term, 5) - await add_word(conn, 4, ' ' + term, term, 1) - - query = await ana.analyze_query(make_phrase(term)) - - assert query.num_token_slots() == 1 - - torder = [(tl.tokens[0].penalty, tl.ttype.name) for tl in query.nodes[0].starting] - torder.sort() - - assert [t[1] for t in torder] == order - - -@pytest.mark.asyncio -async def test_category_words_only_at_beginning(conn): - ana = await tok.create_query_analyzer(conn) - - await add_special_term(conn, 1, 'foo', 'amenity', 'restaurant', 'in') - await add_word(conn, 2, ' bar', 'BAR', 1) - - query = await ana.analyze_query(make_phrase('foo BAR foo')) - - assert query.num_token_slots() == 3 - assert len(query.nodes[0].starting) == 1 - assert query.nodes[0].starting[0].ttype == TokenType.NEAR_ITEM - assert not query.nodes[2].starting - - -@pytest.mark.asyncio -async def test_qualifier_words(conn): - ana = await tok.create_query_analyzer(conn) - - await add_special_term(conn, 1, 'foo', 'amenity', 'restaurant', '-') - await add_word(conn, 2, ' bar', 'w', None) - - query = await ana.analyze_query(make_phrase('foo BAR foo BAR foo')) - - assert query.num_token_slots() == 5 - assert set(t.ttype for t in query.nodes[0].starting) == {TokenType.NEAR_ITEM, TokenType.QUALIFIER} - assert set(t.ttype for t in query.nodes[2].starting) == {TokenType.QUALIFIER} - assert set(t.ttype for t in query.nodes[4].starting) == {TokenType.NEAR_ITEM, TokenType.QUALIFIER} - - -@pytest.mark.asyncio -@pytest.mark.parametrize('logtype', ['text', 'html']) -async def test_log_output(conn, logtype): - ana = await tok.create_query_analyzer(conn) - - await add_word(conn, 1, 'foo', 'FOO', 99) - - set_log_output(logtype) - await ana.analyze_query(make_phrase('foo')) - - assert get_and_disable() diff --git a/test/python/api/search/test_token_assignment.py b/test/python/api/search/test_token_assignment.py index 884d2932..0d89ed5f 100644 --- a/test/python/api/search/test_token_assignment.py +++ b/test/python/api/search/test_token_assignment.py @@ -20,7 +20,7 @@ class MyToken(Token): def make_query(*args): q = QueryStruct([Phrase(args[0][1], '')]) dummy = MyToken(penalty=3.0, token=45, count=1, addr_count=1, - lookup_word='foo', is_indexed=True) + lookup_word='foo') for btype, ptype, _ in args[1:]: q.add_node(btype, ptype) diff --git a/test/python/api/test_api_status.py b/test/python/api/test_api_status.py index 5412ca6e..9341b527 100644 --- a/test/python/api/test_api_status.py +++ b/test/python/api/test_api_status.py @@ -7,7 +7,6 @@ """ Tests for the status API call. """ -from pathlib import Path import datetime as dt import pytest @@ -46,7 +45,7 @@ def test_status_full(apiobj, frontend): def test_status_database_not_found(monkeypatch): monkeypatch.setenv('NOMINATIM_DATABASE_DSN', 'dbname=rgjdfkgjedkrgdfkngdfkg') - api = napi.NominatimAPI(Path('/invalid'), {}) + api = napi.NominatimAPI() result = api.status() diff --git a/test/python/api/test_export.py b/test/python/api/test_export.py index 1d9bf90f..b0da52ce 100644 --- a/test/python/api/test_export.py +++ b/test/python/api/test_export.py @@ -14,8 +14,7 @@ import nominatim_db.cli @pytest.fixture def run_export(tmp_path, capsys): def _exec(args): - assert 0 == nominatim_db.cli.nominatim(module_dir='MODULE NOT AVAILABLE', - osm2pgsql_path='OSM2PGSQL NOT AVAILABLE', + assert 0 == nominatim_db.cli.nominatim(osm2pgsql_path='OSM2PGSQL NOT AVAILABLE', cli_args=['export', '--project-dir', str(tmp_path)] + args) return capsys.readouterr().out.split('\r\n') diff --git a/test/python/api/test_server_glue_v1.py b/test/python/api/test_server_glue_v1.py index 5ef16904..6ea790c0 100644 --- a/test/python/api/test_server_glue_v1.py +++ b/test/python/api/test_server_glue_v1.py @@ -9,7 +9,6 @@ Tests for the Python web frameworks adaptor, v1 API. """ import json import xml.etree.ElementTree as ET -from pathlib import Path import pytest @@ -242,7 +241,7 @@ class TestStatusEndpoint: a = FakeAdaptor() self.status = napi.StatusResult(0, 'foo') - resp = await glue.status_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + resp = await glue.status_endpoint(napi.NominatimAPIAsync(), a) assert isinstance(resp, FakeResponse) assert resp.status == 200 @@ -254,7 +253,7 @@ class TestStatusEndpoint: a = FakeAdaptor() self.status = napi.StatusResult(405, 'foo') - resp = await glue.status_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + resp = await glue.status_endpoint(napi.NominatimAPIAsync(), a) assert isinstance(resp, FakeResponse) assert resp.status == 500 @@ -266,7 +265,7 @@ class TestStatusEndpoint: a = FakeAdaptor(params={'format': 'json'}) self.status = napi.StatusResult(405, 'foo') - resp = await glue.status_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + resp = await glue.status_endpoint(napi.NominatimAPIAsync(), a) assert isinstance(resp, FakeResponse) assert resp.status == 200 @@ -279,7 +278,7 @@ class TestStatusEndpoint: self.status = napi.StatusResult(0, 'foo') with pytest.raises(FakeError): - await glue.status_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + await glue.status_endpoint(napi.NominatimAPIAsync(), a) # details_endpoint() @@ -305,14 +304,14 @@ class TestDetailsEndpoint: a = FakeAdaptor() with pytest.raises(FakeError, match='^400 -- .*Missing'): - await glue.details_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + await glue.details_endpoint(napi.NominatimAPIAsync(), a) @pytest.mark.asyncio async def test_details_by_place_id(self): a = FakeAdaptor(params={'place_id': '4573'}) - await glue.details_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + await glue.details_endpoint(napi.NominatimAPIAsync(), a) assert self.lookup_args[0].place_id == 4573 @@ -321,7 +320,7 @@ class TestDetailsEndpoint: async def test_details_by_osm_id(self): a = FakeAdaptor(params={'osmtype': 'N', 'osmid': '45'}) - await glue.details_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + await glue.details_endpoint(napi.NominatimAPIAsync(), a) assert self.lookup_args[0].osm_type == 'N' assert self.lookup_args[0].osm_id == 45 @@ -332,7 +331,7 @@ class TestDetailsEndpoint: async def test_details_with_debugging(self): a = FakeAdaptor(params={'osmtype': 'N', 'osmid': '45', 'debug': '1'}) - resp = await glue.details_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + resp = await glue.details_endpoint(napi.NominatimAPIAsync(), a) content = ET.fromstring(resp.output) assert resp.content_type == 'text/html; charset=utf-8' @@ -345,7 +344,7 @@ class TestDetailsEndpoint: self.result = None with pytest.raises(FakeError, match='^404 -- .*found'): - await glue.details_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + await glue.details_endpoint(napi.NominatimAPIAsync(), a) # reverse_endpoint() @@ -370,7 +369,7 @@ class TestReverseEndPoint: a.params['format'] = 'xml' with pytest.raises(FakeError, match='^400 -- (?s:.*)missing'): - await glue.reverse_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + await glue.reverse_endpoint(napi.NominatimAPIAsync(), a) @pytest.mark.asyncio @@ -380,7 +379,7 @@ class TestReverseEndPoint: a.params = params a.params['format'] = 'json' - res = await glue.reverse_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + res = await glue.reverse_endpoint(napi.NominatimAPIAsync(), a) assert res == '' @@ -391,7 +390,7 @@ class TestReverseEndPoint: a.params['lat'] = '56.3' a.params['lon'] = '6.8' - assert await glue.reverse_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + assert await glue.reverse_endpoint(napi.NominatimAPIAsync(), a) @pytest.mark.asyncio @@ -400,7 +399,7 @@ class TestReverseEndPoint: a.params['q'] = '34.6 2.56' a.params['format'] = 'json' - res = await glue.search_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + res = await glue.search_endpoint(napi.NominatimAPIAsync(), a) assert len(json.loads(res.output)) == 1 @@ -425,7 +424,7 @@ class TestLookupEndpoint: a = FakeAdaptor() a.params['format'] = 'json' - res = await glue.lookup_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + res = await glue.lookup_endpoint(napi.NominatimAPIAsync(), a) assert res.output == '[]' @@ -437,7 +436,7 @@ class TestLookupEndpoint: a.params['format'] = 'json' a.params['osm_ids'] = f'W34,{param},N33333' - res = await glue.lookup_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + res = await glue.lookup_endpoint(napi.NominatimAPIAsync(), a) assert len(json.loads(res.output)) == 1 @@ -449,7 +448,7 @@ class TestLookupEndpoint: a.params['format'] = 'json' a.params['osm_ids'] = f'W34,{param},N33333' - res = await glue.lookup_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + res = await glue.lookup_endpoint(napi.NominatimAPIAsync(), a) assert len(json.loads(res.output)) == 1 @@ -460,7 +459,7 @@ class TestLookupEndpoint: a.params['format'] = 'json' a.params['osm_ids'] = 'N23,W34' - res = await glue.lookup_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + res = await glue.lookup_endpoint(napi.NominatimAPIAsync(), a) assert len(json.loads(res.output)) == 1 @@ -485,7 +484,7 @@ class TestSearchEndPointSearch: a = FakeAdaptor() a.params['q'] = 'something' - res = await glue.search_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + res = await glue.search_endpoint(napi.NominatimAPIAsync(), a) assert len(json.loads(res.output)) == 1 @@ -496,7 +495,7 @@ class TestSearchEndPointSearch: a.params['q'] = 'something' a.params['format'] = 'xml' - res = await glue.search_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + res = await glue.search_endpoint(napi.NominatimAPIAsync(), a) assert res.status == 200 assert res.output.index('something') > 0 @@ -509,7 +508,7 @@ class TestSearchEndPointSearch: a.params['city'] = 'ignored' with pytest.raises(FakeError, match='^400 -- .*cannot be used together'): - res = await glue.search_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + res = await glue.search_endpoint(napi.NominatimAPIAsync(), a) @pytest.mark.asyncio @@ -521,7 +520,7 @@ class TestSearchEndPointSearch: if not dedupe: a.params['dedupe'] = '0' - res = await glue.search_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + res = await glue.search_endpoint(napi.NominatimAPIAsync(), a) assert len(json.loads(res.output)) == numres @@ -544,7 +543,7 @@ class TestSearchEndPointSearchAddress: a = FakeAdaptor() a.params['street'] = 'something' - res = await glue.search_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + res = await glue.search_endpoint(napi.NominatimAPIAsync(), a) assert len(json.loads(res.output)) == 1 @@ -567,6 +566,6 @@ class TestSearchEndPointSearchCategory: a = FakeAdaptor() a.params['q'] = '[shop=fog]' - res = await glue.search_endpoint(napi.NominatimAPIAsync(Path('/invalid')), a) + res = await glue.search_endpoint(napi.NominatimAPIAsync(), a) assert len(json.loads(res.output)) == 1 diff --git a/test/python/api/test_warm.py b/test/python/api/test_warm.py index 304943f1..f0c9986d 100644 --- a/test/python/api/test_warm.py +++ b/test/python/api/test_warm.py @@ -27,7 +27,6 @@ def setup_database_with_context(apiobj, table_factory): @pytest.mark.parametrize('args', [['--search-only'], ['--reverse-only']]) def test_warm_all(tmp_path, args): - assert 0 == nominatim_db.cli.nominatim(module_dir='MODULE NOT AVAILABLE', - osm2pgsql_path='OSM2PGSQL NOT AVAILABLE', + assert 0 == nominatim_db.cli.nominatim(osm2pgsql_path='OSM2PGSQL NOT AVAILABLE', cli_args=['admin', '--project-dir', str(tmp_path), '--warm'] + args) diff --git a/test/python/cli/conftest.py b/test/python/cli/conftest.py index d5ade223..84f2d659 100644 --- a/test/python/cli/conftest.py +++ b/test/python/cli/conftest.py @@ -68,8 +68,7 @@ def cli_call(): Returns a function that can be called with the desired CLI arguments. """ def _call_nominatim(*args): - return nominatim_db.cli.nominatim(module_dir='MODULE NOT AVAILABLE', - osm2pgsql_path='OSM2PGSQL NOT AVAILABLE', + return nominatim_db.cli.nominatim(osm2pgsql_path='OSM2PGSQL NOT AVAILABLE', cli_args=args) return _call_nominatim diff --git a/test/python/cli/test_cli.py b/test/python/cli/test_cli.py index 2831f84f..d42df50a 100644 --- a/test/python/cli/test_cli.py +++ b/test/python/cli/test_cli.py @@ -37,15 +37,6 @@ def test_cli_version(cli_call, capsys): assert captured.out.startswith('Nominatim version') -def test_cli_serve_php(cli_call, mock_func_factory): - func = mock_func_factory(nominatim_db.cli, 'run_php_server') - - cli_call('serve', '--engine', 'php') == 0 - - assert func.called == 1 - - - class TestCliWithDb: @pytest.fixture(autouse=True) diff --git a/test/python/cli/test_cmd_import.py b/test/python/cli/test_cmd_import.py index e47d713c..f833dde3 100644 --- a/test/python/cli/test_cmd_import.py +++ b/test/python/cli/test_cmd_import.py @@ -52,7 +52,6 @@ class TestCliImportWithDb: mock_func_factory(nominatim_db.tools.refresh, 'load_address_levels_from_config'), mock_func_factory(nominatim_db.tools.postcodes, 'update_postcodes'), async_mock_func_factory(nominatim_db.indexer.indexer.Indexer, 'index_full'), - mock_func_factory(nominatim_db.tools.refresh, 'setup_website'), ] params = ['import', '--osm-file', __file__] @@ -81,7 +80,6 @@ class TestCliImportWithDb: mock_func_factory(nominatim_db.data.country_info, 'create_country_names'), mock_func_factory(nominatim_db.tools.postcodes, 'update_postcodes'), async_mock_func_factory(nominatim_db.indexer.indexer.Indexer, 'index_full'), - mock_func_factory(nominatim_db.tools.refresh, 'setup_website'), mock_func_factory(nominatim_db.db.properties, 'set_property') ] @@ -98,7 +96,6 @@ class TestCliImportWithDb: async_mock_func_factory(nominatim_db.tools.database_import, 'create_search_indices'), mock_func_factory(nominatim_db.data.country_info, 'create_country_names'), async_mock_func_factory(nominatim_db.indexer.indexer.Indexer, 'index_full'), - mock_func_factory(nominatim_db.tools.refresh, 'setup_website'), mock_func_factory(nominatim_db.db.properties, 'set_property') ] @@ -115,7 +112,6 @@ class TestCliImportWithDb: mocks = [ async_mock_func_factory(nominatim_db.tools.database_import, 'create_search_indices'), mock_func_factory(nominatim_db.data.country_info, 'create_country_names'), - mock_func_factory(nominatim_db.tools.refresh, 'setup_website'), mock_func_factory(nominatim_db.db.properties, 'set_property') ] diff --git a/test/python/cli/test_cmd_refresh.py b/test/python/cli/test_cmd_refresh.py index 9074b2cc..9f3d7bb2 100644 --- a/test/python/cli/test_cmd_refresh.py +++ b/test/python/cli/test_cmd_refresh.py @@ -25,7 +25,6 @@ class TestRefresh: ('address-levels', 'load_address_levels_from_config'), ('wiki-data', 'import_wikipedia_articles'), ('importance', 'recompute_importance'), - ('website', 'setup_website'), ]) def test_refresh_command(self, mock_func_factory, command, func): mock_func_factory(nominatim_db.tools.refresh, 'create_functions') diff --git a/test/python/config/test_config.py b/test/python/config/test_config.py index 5c9393ec..8f90b5da 100644 --- a/test/python/config/test_config.py +++ b/test/python/config/test_config.py @@ -140,8 +140,8 @@ def test_get_bool(make_config, monkeypatch, value, result): def test_get_bool_empty(make_config): config = make_config() - assert config.DATABASE_MODULE_PATH == '' - assert not config.get_bool('DATABASE_MODULE_PATH') + assert config.TOKENIZER_CONFIG == '' + assert not config.get_bool('TOKENIZER_CONFIG') @pytest.mark.parametrize("value,result", [('0', 0), ('1', 1), @@ -167,10 +167,10 @@ def test_get_int_bad_values(make_config, monkeypatch, value): def test_get_int_empty(make_config): config = make_config() - assert config.DATABASE_MODULE_PATH == '' + assert config.TOKENIZER_CONFIG == '' with pytest.raises(UsageError): - config.get_int('DATABASE_MODULE_PATH') + config.get_int('TOKENIZER_CONFIG') @pytest.mark.parametrize("value,outlist", [('sd', ['sd']), @@ -193,8 +193,8 @@ def test_get_str_list_empty(make_config): def test_get_path_empty(make_config): config = make_config() - assert config.DATABASE_MODULE_PATH == '' - assert not config.get_path('DATABASE_MODULE_PATH') + assert config.TOKENIZER_CONFIG == '' + assert not config.get_path('TOKENIZER_CONFIG') def test_get_path_absolute(make_config, monkeypatch): diff --git a/test/python/conftest.py b/test/python/conftest.py index 3ced3205..a25ff8ec 100644 --- a/test/python/conftest.py +++ b/test/python/conftest.py @@ -109,7 +109,7 @@ def table_factory(temp_db_conn): @pytest.fixture def def_config(): cfg = Configuration(None) - cfg.set_libdirs(module='.', osm2pgsql='.') + cfg.set_libdirs(osm2pgsql=None) return cfg @@ -118,7 +118,7 @@ def project_env(tmp_path): projdir = tmp_path / 'project' projdir.mkdir() cfg = Configuration(projdir) - cfg.set_libdirs(module='.', osm2pgsql='.') + cfg.set_libdirs(osm2pgsql=None) return cfg @@ -208,7 +208,7 @@ def osmline_table(temp_db_with_extensions, table_factory): def sql_preprocessor_cfg(tmp_path, table_factory, temp_db_with_extensions): table_factory('country_name', 'partition INT', ((0, ), (1, ), (2, ))) cfg = Configuration(None) - cfg.set_libdirs(module='.', osm2pgsql='.', sql=tmp_path) + cfg.set_libdirs(osm2pgsql=None, sql=tmp_path) return cfg diff --git a/test/python/mock_legacy_word_table.py b/test/python/mock_legacy_word_table.py deleted file mode 100644 index d3f81a4d..00000000 --- a/test/python/mock_legacy_word_table.py +++ /dev/null @@ -1,99 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# -# This file is part of Nominatim. (https://nominatim.org) -# -# Copyright (C) 2024 by the Nominatim developer community. -# For a full list of authors see the git log. -""" -Legacy word table for testing with functions to prefil and test contents -of the table. -""" -from nominatim_db.db.connection import execute_scalar - -class MockLegacyWordTable: - """ A word table for testing using legacy word table structure. - """ - def __init__(self, conn): - self.conn = conn - with conn.cursor() as cur: - cur.execute("""CREATE TABLE word (word_id INTEGER, - word_token text, - word text, - class text, - type text, - country_code varchar(2), - search_name_count INTEGER, - operator TEXT)""") - - conn.commit() - - def add_full_word(self, word_id, word, word_token=None): - with self.conn.cursor() as cur: - cur.execute("""INSERT INTO word (word_id, word_token, word) - VALUES (%s, %s, %s) - """, (word_id, ' ' + (word_token or word), word)) - self.conn.commit() - - - def add_special(self, word_token, word, cls, typ, oper): - with self.conn.cursor() as cur: - cur.execute("""INSERT INTO word (word_token, word, class, type, operator) - VALUES (%s, %s, %s, %s, %s) - """, (word_token, word, cls, typ, oper)) - self.conn.commit() - - - def add_country(self, country_code, word_token): - with self.conn.cursor() as cur: - cur.execute("INSERT INTO word (word_token, country_code) VALUES(%s, %s)", - (word_token, country_code)) - self.conn.commit() - - - def add_postcode(self, word_token, postcode): - with self.conn.cursor() as cur: - cur.execute("""INSERT INTO word (word_token, word, class, type) - VALUES (%s, %s, 'place', 'postcode') - """, (word_token, postcode)) - self.conn.commit() - - - def count(self): - return execute_scalar(self.conn, "SELECT count(*) FROM word") - - - def count_special(self): - return execute_scalar(self.conn, "SELECT count(*) FROM word WHERE class != 'place'") - - - def get_special(self): - with self.conn.cursor() as cur: - cur.execute("""SELECT word_token, word, class as cls, type, operator - FROM word WHERE class != 'place'""") - result = set((tuple(row) for row in cur)) - assert len(result) == cur.rowcount, "Word table has duplicates." - return result - - - def get_country(self): - with self.conn.cursor() as cur: - cur.execute("""SELECT country_code, word_token - FROM word WHERE country_code is not null""") - result = set((tuple(row) for row in cur)) - assert len(result) == cur.rowcount, "Word table has duplicates." - return result - - - def get_postcodes(self): - with self.conn.cursor() as cur: - cur.execute("""SELECT word FROM word - WHERE class = 'place' and type = 'postcode'""") - return set((row[0] for row in cur)) - - def get_partial_words(self): - with self.conn.cursor() as cur: - cur.execute("""SELECT word_token, search_name_count FROM word - WHERE class is null and country_code is null - and not word_token like ' %'""") - return set((tuple(row) for row in cur)) - diff --git a/test/python/tokenizer/test_legacy.py b/test/python/tokenizer/test_legacy.py deleted file mode 100644 index bf208c92..00000000 --- a/test/python/tokenizer/test_legacy.py +++ /dev/null @@ -1,591 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# -# This file is part of Nominatim. (https://nominatim.org) -# -# Copyright (C) 2024 by the Nominatim developer community. -# For a full list of authors see the git log. -""" -Test for legacy tokenizer. -""" -import shutil -import re - -import pytest - -from nominatim_db.data.place_info import PlaceInfo -from nominatim_db.tokenizer import legacy_tokenizer -from nominatim_db.db import properties -from nominatim_db.errors import UsageError - -from mock_legacy_word_table import MockLegacyWordTable - -# Force use of legacy word table -@pytest.fixture -def word_table(temp_db_conn): - return MockLegacyWordTable(temp_db_conn) - - -@pytest.fixture -def test_config(project_env, tmp_path): - module_dir = tmp_path / 'module_src' - module_dir.mkdir() - (module_dir / 'nominatim.so').write_text('TEST nominatim.so') - - project_env.lib_dir.module = module_dir - - sqldir = tmp_path / 'sql' - sqldir.mkdir() - (sqldir / 'tokenizer').mkdir() - - # Get the original SQL but replace make_standard_name to avoid module use. - init_sql = (project_env.lib_dir.sql / 'tokenizer' / 'legacy_tokenizer.sql').read_text() - for fn in ('transliteration', 'gettokenstring'): - init_sql = re.sub(f'CREATE OR REPLACE FUNCTION {fn}[^;]*;', - '', init_sql, re.DOTALL) - init_sql += """ - CREATE OR REPLACE FUNCTION make_standard_name(name TEXT) - RETURNS TEXT AS $$ SELECT lower(name); $$ LANGUAGE SQL; - - """ - # Also load util functions. Some are needed by the tokenizer. - init_sql += (project_env.lib_dir.sql / 'functions' / 'utils.sql').read_text() - (sqldir / 'tokenizer' / 'legacy_tokenizer.sql').write_text(init_sql) - - (sqldir / 'words.sql').write_text("SELECT 'a'") - - shutil.copy(str(project_env.lib_dir.sql / 'tokenizer' / 'legacy_tokenizer_tables.sql'), - str(sqldir / 'tokenizer' / 'legacy_tokenizer_tables.sql')) - - project_env.lib_dir.sql = sqldir - project_env.lib_dir.data = sqldir - - return project_env - - -@pytest.fixture -def tokenizer_factory(dsn, tmp_path, property_table): - (tmp_path / 'tokenizer').mkdir() - - def _maker(): - return legacy_tokenizer.create(dsn, tmp_path / 'tokenizer') - - return _maker - - -@pytest.fixture -def tokenizer_setup(tokenizer_factory, test_config, monkeypatch, sql_preprocessor): - monkeypatch.setattr(legacy_tokenizer, '_check_module', lambda m, c: None) - tok = tokenizer_factory() - tok.init_new_db(test_config) - - -@pytest.fixture -def analyzer(tokenizer_factory, test_config, monkeypatch, sql_preprocessor, - word_table, temp_db_with_extensions, tmp_path): - monkeypatch.setattr(legacy_tokenizer, '_check_module', lambda m, c: None) - monkeypatch.setenv('NOMINATIM_TERM_NORMALIZATION', ':: lower();') - tok = tokenizer_factory() - tok.init_new_db(test_config) - monkeypatch.undo() - - with tok.name_analyzer() as analyzer: - yield analyzer - - -@pytest.fixture -def make_standard_name(temp_db_cursor): - temp_db_cursor.execute("""CREATE OR REPLACE FUNCTION make_standard_name(name TEXT) - RETURNS TEXT AS $$ SELECT '#' || lower(name) || '#'; $$ LANGUAGE SQL""") - - -@pytest.fixture -def create_postcode_id(temp_db_cursor): - temp_db_cursor.execute("""CREATE OR REPLACE FUNCTION create_postcode_id(postcode TEXT) - RETURNS BOOLEAN AS $$ - INSERT INTO word (word_token, word, class, type) - VALUES (' ' || postcode, postcode, 'place', 'postcode') - RETURNING True; - $$ LANGUAGE SQL""") - - -def test_init_new(tokenizer_factory, test_config, monkeypatch, - temp_db_conn, sql_preprocessor): - monkeypatch.setenv('NOMINATIM_TERM_NORMALIZATION', 'xxvv') - monkeypatch.setattr(legacy_tokenizer, '_check_module', lambda m, c: None) - - tok = tokenizer_factory() - tok.init_new_db(test_config) - - assert properties.get_property(temp_db_conn, legacy_tokenizer.DBCFG_NORMALIZATION) == 'xxvv' - - outfile = test_config.project_dir / 'module' / 'nominatim.so' - - assert outfile.exists() - assert outfile.read_text() == 'TEST nominatim.so' - assert outfile.stat().st_mode == 33261 - - -def test_init_module_load_failed(tokenizer_factory, test_config): - tok = tokenizer_factory() - - with pytest.raises(UsageError): - tok.init_new_db(test_config) - - -def test_init_module_custom(tokenizer_factory, test_config, - monkeypatch, tmp_path, sql_preprocessor): - module_dir = (tmp_path / 'custom').resolve() - module_dir.mkdir() - (module_dir/ 'nominatim.so').write_text('CUSTOM nomiantim.so') - - monkeypatch.setenv('NOMINATIM_DATABASE_MODULE_PATH', str(module_dir)) - monkeypatch.setattr(legacy_tokenizer, '_check_module', lambda m, c: None) - - tok = tokenizer_factory() - tok.init_new_db(test_config) - - assert not (test_config.project_dir / 'module').exists() - - -def test_init_from_project(tokenizer_setup, tokenizer_factory, test_config): - tok = tokenizer_factory() - - tok.init_from_project(test_config) - - assert tok.normalization is not None - - -def test_update_sql_functions(sql_preprocessor, temp_db_conn, - tokenizer_factory, test_config, table_factory, - monkeypatch, temp_db_cursor): - monkeypatch.setenv('NOMINATIM_MAX_WORD_FREQUENCY', '1133') - monkeypatch.setattr(legacy_tokenizer, '_check_module', lambda m, c: None) - tok = tokenizer_factory() - tok.init_new_db(test_config) - monkeypatch.undo() - - assert properties.get_property(temp_db_conn, legacy_tokenizer.DBCFG_MAXWORDFREQ) == '1133' - - table_factory('test', 'txt TEXT') - - func_file = test_config.lib_dir.sql / 'tokenizer' / 'legacy_tokenizer.sql' - func_file.write_text("""INSERT INTO test VALUES ('{{max_word_freq}}'), - ('{{modulepath}}')""") - - tok.update_sql_functions(test_config) - - test_content = temp_db_cursor.row_set('SELECT * FROM test') - assert test_content == set((('1133', ), (str(test_config.project_dir / 'module'), ))) - - -def test_finalize_import(tokenizer_factory, temp_db_conn, - temp_db_cursor, test_config, monkeypatch, - sql_preprocessor_cfg): - monkeypatch.setattr(legacy_tokenizer, '_check_module', lambda m, c: None) - - func_file = test_config.lib_dir.sql / 'tokenizer' / 'legacy_tokenizer_indices.sql' - func_file.write_text("""CREATE FUNCTION test() RETURNS TEXT - AS $$ SELECT 'b'::text $$ LANGUAGE SQL""") - - tok = tokenizer_factory() - tok.init_new_db(test_config) - - tok.finalize_import(test_config) - - temp_db_cursor.scalar('SELECT test()') == 'b' - - -def test_migrate_database(tokenizer_factory, test_config, temp_db_conn, monkeypatch): - monkeypatch.setattr(legacy_tokenizer, '_check_module', lambda m, c: None) - tok = tokenizer_factory() - tok.migrate_database(test_config) - - assert properties.get_property(temp_db_conn, legacy_tokenizer.DBCFG_MAXWORDFREQ) is not None - assert properties.get_property(temp_db_conn, legacy_tokenizer.DBCFG_NORMALIZATION) is not None - - outfile = test_config.project_dir / 'module' / 'nominatim.so' - - assert outfile.exists() - assert outfile.read_text() == 'TEST nominatim.so' - assert outfile.stat().st_mode == 33261 - - -def test_check_database(test_config, tokenizer_factory, monkeypatch, - temp_db_cursor, sql_preprocessor_cfg): - monkeypatch.setattr(legacy_tokenizer, '_check_module', lambda m, c: None) - tok = tokenizer_factory() - tok.init_new_db(test_config) - - assert tok.check_database(False) is None - - -def test_check_database_no_tokenizer(test_config, tokenizer_factory): - tok = tokenizer_factory() - - assert tok.check_database(False) is not None - - -def test_check_database_bad_setup(test_config, tokenizer_factory, monkeypatch, - temp_db_cursor, sql_preprocessor_cfg): - monkeypatch.setattr(legacy_tokenizer, '_check_module', lambda m, c: None) - tok = tokenizer_factory() - tok.init_new_db(test_config) - - # Inject a bad transliteration. - temp_db_cursor.execute("""CREATE OR REPLACE FUNCTION make_standard_name(name TEXT) - RETURNS TEXT AS $$ SELECT 'garbage'::text; $$ LANGUAGE SQL""") - - assert tok.check_database(False) is not None - - -def test_update_statistics_reverse_only(word_table, tokenizer_factory, test_config): - tok = tokenizer_factory() - tok.update_statistics(test_config) - - -def test_update_statistics(word_table, table_factory, temp_db_cursor, tokenizer_factory, test_config): - word_table.add_full_word(1000, 'hello') - table_factory('search_name', - 'place_id BIGINT, name_vector INT[]', - [(12, [1000])]) - tok = tokenizer_factory() - - tok.update_statistics(test_config) - - assert temp_db_cursor.scalar("""SELECT count(*) FROM word - WHERE word_token like ' %' and - search_name_count > 0""") > 0 - - -def test_update_word_tokens(tokenizer_factory): - tok = tokenizer_factory() - - # This is a noop and should just pass. - tok.update_word_tokens() - - -def test_normalize(analyzer): - assert analyzer.normalize('TEsT') == 'test' - - -def test_update_postcodes_from_db_empty(analyzer, table_factory, word_table, - create_postcode_id): - table_factory('location_postcode', 'postcode TEXT', - content=(('1234',), ('12 34',), ('AB23',), ('1234',))) - - analyzer.update_postcodes_from_db() - - assert word_table.get_postcodes() == {'1234', '12 34', 'AB23'} - - -def test_update_postcodes_from_db_add_and_remove(analyzer, table_factory, word_table, - create_postcode_id): - table_factory('location_postcode', 'postcode TEXT', - content=(('1234',), ('45BC', ), ('XX45', ))) - word_table.add_postcode(' 1234', '1234') - word_table.add_postcode(' 5678', '5678') - - analyzer.update_postcodes_from_db() - - assert word_table.get_postcodes() == {'1234', '45BC', 'XX45'} - - -def test_update_special_phrase_empty_table(analyzer, word_table, make_standard_name): - analyzer.update_special_phrases([ - ("König bei", "amenity", "royal", "near"), - ("Könige", "amenity", "royal", "-"), - ("könige", "amenity", "royal", "-"), - ("strasse", "highway", "primary", "in") - ], True) - - assert word_table.get_special() \ - == set(((' #könig bei#', 'könig bei', 'amenity', 'royal', 'near'), - (' #könige#', 'könige', 'amenity', 'royal', None), - (' #strasse#', 'strasse', 'highway', 'primary', 'in'))) - - -def test_update_special_phrase_delete_all(analyzer, word_table, make_standard_name): - word_table.add_special(' #foo#', 'foo', 'amenity', 'prison', 'in') - word_table.add_special(' #bar#', 'bar', 'highway', 'road', None) - - assert word_table.count_special() == 2 - - analyzer.update_special_phrases([], True) - - assert word_table.count_special() == 0 - - -def test_update_special_phrases_no_replace(analyzer, word_table, make_standard_name): - word_table.add_special(' #foo#', 'foo', 'amenity', 'prison', 'in') - word_table.add_special(' #bar#', 'bar', 'highway', 'road', None) - - assert word_table.count_special() == 2 - - analyzer.update_special_phrases([], False) - - assert word_table.count_special() == 2 - - -def test_update_special_phrase_modify(analyzer, word_table, make_standard_name): - word_table.add_special(' #foo#', 'foo', 'amenity', 'prison', 'in') - word_table.add_special(' #bar#', 'bar', 'highway', 'road', None) - - assert word_table.count_special() == 2 - - analyzer.update_special_phrases([ - ('prison', 'amenity', 'prison', 'in'), - ('bar', 'highway', 'road', '-'), - ('garden', 'leisure', 'garden', 'near') - ], True) - - assert word_table.get_special() \ - == set(((' #prison#', 'prison', 'amenity', 'prison', 'in'), - (' #bar#', 'bar', 'highway', 'road', None), - (' #garden#', 'garden', 'leisure', 'garden', 'near'))) - - -def test_add_country_names(analyzer, word_table, make_standard_name): - analyzer.add_country_names('de', {'name': 'Germany', - 'name:de': 'Deutschland', - 'short_name': 'germany'}) - - assert word_table.get_country() \ - == {('de', ' #germany#'), - ('de', ' #deutschland#')} - - -def test_add_more_country_names(analyzer, word_table, make_standard_name): - word_table.add_country('fr', ' #france#') - word_table.add_country('it', ' #italy#') - word_table.add_country('it', ' #itala#') - - analyzer.add_country_names('it', {'name': 'Italy', 'ref': 'IT'}) - - assert word_table.get_country() \ - == {('fr', ' #france#'), - ('it', ' #italy#'), - ('it', ' #itala#'), - ('it', ' #it#')} - - -@pytest.mark.parametrize('pcode', ['12345', 'AB 123', '34-345']) -def test_process_place_postcode(analyzer, create_postcode_id, word_table, pcode): - analyzer.process_place(PlaceInfo({'address': {'postcode' : pcode}})) - - assert word_table.get_postcodes() == {pcode, } - - -@pytest.mark.parametrize('pcode', ['12:23', 'ab;cd;f', '123;836']) -def test_process_place_bad_postcode(analyzer, create_postcode_id, word_table, pcode): - analyzer.process_place(PlaceInfo({'address': {'postcode' : pcode}})) - - assert not word_table.get_postcodes() - - -class TestHousenumberName: - - @staticmethod - @pytest.fixture(autouse=True) - def setup_create_housenumbers(temp_db_cursor): - temp_db_cursor.execute("""CREATE OR REPLACE FUNCTION create_housenumbers( - housenumbers TEXT[], - OUT tokens TEXT, OUT normtext TEXT) - AS $$ - SELECT housenumbers::TEXT, array_to_string(housenumbers, ';') - $$ LANGUAGE SQL""") - - - @staticmethod - @pytest.mark.parametrize('hnr', ['123a', '1', '101']) - def test_process_place_housenumbers_simple(analyzer, hnr): - info = analyzer.process_place(PlaceInfo({'address': {'housenumber' : hnr}})) - - assert info['hnr'] == hnr - assert info['hnr_tokens'].startswith("{") - - - @staticmethod - def test_process_place_housenumbers_lists(analyzer): - info = analyzer.process_place(PlaceInfo({'address': {'conscriptionnumber' : '1; 2;3'}})) - - assert set(info['hnr'].split(';')) == set(('1', '2', '3')) - - - @staticmethod - def test_process_place_housenumbers_duplicates(analyzer): - info = analyzer.process_place(PlaceInfo({'address': {'housenumber' : '134', - 'conscriptionnumber' : '134', - 'streetnumber' : '99a'}})) - - assert set(info['hnr'].split(';')) == set(('134', '99a')) - - -class TestPlaceNames: - - @pytest.fixture(autouse=True) - def setup(self, analyzer): - self.analyzer = analyzer - - - def expect_name_terms(self, info, *expected_terms): - tokens = self.analyzer.get_word_token_info(list(expected_terms)) - for token in tokens: - assert token[2] is not None, "No token for {0}".format(token) - - assert eval(info['names']) == set((t[2] for t in tokens)),\ - f"Expected: {tokens}\nGot: {info['names']}" - - - def process_named_place(self, names): - return self.analyzer.process_place(PlaceInfo({'name': names})) - - - def test_simple_names(self): - info = self.process_named_place({'name': 'Soft bAr', 'ref': '34'}) - - self.expect_name_terms(info, '#Soft bAr', '#34', 'Soft', 'bAr', '34') - - - @pytest.mark.parametrize('sep', [',' , ';']) - def test_names_with_separator(self, sep): - info = self.process_named_place({'name': sep.join(('New York', 'Big Apple'))}) - - self.expect_name_terms(info, '#New York', '#Big Apple', - 'new', 'york', 'big', 'apple') - - - def test_full_names_with_bracket(self): - info = self.process_named_place({'name': 'Houseboat (left)'}) - - self.expect_name_terms(info, '#Houseboat (left)', '#Houseboat', - 'houseboat', '(left)') - - - def test_country_name(self, word_table): - place = PlaceInfo({'name' : {'name': 'Norge'}, - 'country_code': 'no', - 'rank_address': 4, - 'class': 'boundary', - 'type': 'administrative'}) - - info = self.analyzer.process_place(place) - - self.expect_name_terms(info, '#norge', 'norge') - assert word_table.get_country() == {('no', ' norge')} - - -class TestPlaceAddress: - - @pytest.fixture(autouse=True) - def setup(self, analyzer): - self.analyzer = analyzer - - - @pytest.fixture - def getorcreate_hnr_id(self, temp_db_cursor): - temp_db_cursor.execute("""CREATE SEQUENCE seq_hnr start 1; - CREATE OR REPLACE FUNCTION getorcreate_housenumber_id(lookup_word TEXT) - RETURNS INTEGER AS $$ - SELECT -nextval('seq_hnr')::INTEGER; $$ LANGUAGE SQL""") - - def process_address(self, **kwargs): - return self.analyzer.process_place(PlaceInfo({'address': kwargs})) - - - def name_token_set(self, *expected_terms): - tokens = self.analyzer.get_word_token_info(list(expected_terms)) - for token in tokens: - assert token[2] is not None, "No token for {0}".format(token) - - return set((t[2] for t in tokens)) - - - @pytest.mark.parametrize('pcode', ['12345', 'AB 123', '34-345']) - def test_process_place_postcode(self, word_table, pcode): - self.process_address(postcode=pcode) - - assert word_table.get_postcodes() == {pcode, } - - - @pytest.mark.parametrize('pcode', ['12:23', 'ab;cd;f', '123;836']) - def test_process_place_bad_postcode(self, word_table, pcode): - self.process_address(postcode=pcode) - - assert not word_table.get_postcodes() - - - @pytest.mark.parametrize('hnr', ['123a', '0', '101']) - def test_process_place_housenumbers_simple(self, hnr, getorcreate_hnr_id): - info = self.process_address(housenumber=hnr) - - assert info['hnr'] == hnr.lower() - assert info['hnr_tokens'] == "{-1}" - - - def test_process_place_housenumbers_lists(self, getorcreate_hnr_id): - info = self.process_address(conscriptionnumber='1; 2;3') - - assert set(info['hnr'].split(';')) == set(('1', '2', '3')) - assert info['hnr_tokens'] == "{-1,-2,-3}" - - - def test_process_place_housenumbers_duplicates(self, getorcreate_hnr_id): - info = self.process_address(housenumber='134', - conscriptionnumber='134', - streetnumber='99A') - - assert set(info['hnr'].split(';')) == set(('134', '99a')) - assert info['hnr_tokens'] == "{-1,-2}" - - - def test_process_place_street(self): - # legacy tokenizer only indexes known names - self.analyzer.process_place(PlaceInfo({'name': {'name' : 'Grand Road'}})) - info = self.process_address(street='Grand Road') - - assert eval(info['street']) == self.name_token_set('#Grand Road') - - - def test_process_place_street_empty(self): - info = self.process_address(street='🜵') - - assert info['street'] == '{}' - - - def test_process_place_place(self): - self.analyzer.process_place(PlaceInfo({'name': {'name' : 'Honu Lulu'}})) - info = self.process_address(place='Honu Lulu') - - assert eval(info['place_search']) == self.name_token_set('#Honu Lulu', - 'Honu', 'Lulu') - assert eval(info['place_match']) == self.name_token_set('#Honu Lulu') - - - def test_process_place_place_empty(self): - info = self.process_address(place='🜵') - - assert 'place' not in info - - - def test_process_place_address_terms(self): - for name in ('Zwickau', 'Haupstraße', 'Sachsen'): - self.analyzer.process_place(PlaceInfo({'name': {'name' : name}})) - info = self.process_address(country='de', city='Zwickau', state='Sachsen', - suburb='Zwickau', street='Hauptstr', - full='right behind the church') - - city = self.name_token_set('ZWICKAU') - state = self.name_token_set('SACHSEN') - - print(info) - result = {k: eval(v[0]) for k,v in info['addr'].items()} - - assert result == {'city': city, 'suburb': city, 'state': state} - - - def test_process_place_address_terms_empty(self): - info = self.process_address(country='de', city=' ', street='Hauptstr', - full='right behind the church') - - assert 'addr' not in info - diff --git a/test/python/tools/test_migration.py b/test/python/tools/test_migration.py index 2c7b2d56..0b4d2ec6 100644 --- a/test/python/tools/test_migration.py +++ b/test/python/tools/test_migration.py @@ -14,8 +14,6 @@ from nominatim_db.errors import UsageError from nominatim_db.db.connection import server_version_tuple import nominatim_db.version -from mock_legacy_word_table import MockLegacyWordTable - class DummyTokenizer: def update_sql_functions(self, config): @@ -28,40 +26,14 @@ def postprocess_mock(monkeypatch): monkeypatch.setattr(migration.tokenizer_factory, 'get_tokenizer_for_db', lambda *args: DummyTokenizer()) -@pytest.fixture -def legacy_word_table(temp_db_conn): - return MockLegacyWordTable(temp_db_conn) - -def test_no_migration_old_versions(temp_db_with_extensions, table_factory, def_config): - table_factory('country_name', 'name HSTORE, country_code TEXT') +def test_no_migration_old_versions(temp_db_with_extensions, def_config, property_table): + property_table.set('database_version', '4.2.99-0') with pytest.raises(UsageError, match='Migration not possible'): migration.migrate(def_config, {}) -def test_set_up_migration_for_36(temp_db_with_extensions, temp_db_cursor, - table_factory, def_config, monkeypatch, - postprocess_mock): - # don't actually run any migration, except the property table creation - monkeypatch.setattr(migration, '_MIGRATION_FUNCTIONS', - [((3, 5, 0, 99), migration.add_nominatim_property_table)]) - # Use a r/o user name that always exists - monkeypatch.setenv('NOMINATIM_DATABASE_WEBUSER', 'postgres') - - table_factory('country_name', 'name HSTORE, country_code TEXT', - (({str(x): 'a' for x in range(200)}, 'gb'),)) - - assert not temp_db_cursor.table_exists('nominatim_properties') - - assert migration.migrate(def_config, {}) == 0 - - assert temp_db_cursor.table_exists('nominatim_properties') - - assert 1 == temp_db_cursor.scalar(""" SELECT count(*) FROM nominatim_properties - WHERE property = 'database_version'""") - - def test_already_at_version(temp_db_with_extensions, def_config, property_table): property_table.set('database_version', @@ -72,8 +44,7 @@ def test_already_at_version(temp_db_with_extensions, def_config, property_table) def test_run_single_migration(temp_db_with_extensions, def_config, temp_db_cursor, property_table, monkeypatch, postprocess_mock): - oldversion = [x for x in nominatim_db.version.NOMINATIM_VERSION] - oldversion[0] -= 1 + oldversion = [4, 4, 99, 0] property_table.set('database_version', str(nominatim_db.version.NominatimVersion(*oldversion))) @@ -86,7 +57,7 @@ def test_run_single_migration(temp_db_with_extensions, def_config, temp_db_curso """ Dummy migration""" done['old'] = True - oldversion[0] = 0 + oldversion[1] = 0 monkeypatch.setattr(migration, '_MIGRATION_FUNCTIONS', [(tuple(oldversion), _old_migration), (nominatim_db.version.NOMINATIM_VERSION, _migration)]) @@ -103,131 +74,3 @@ def test_run_single_migration(temp_db_with_extensions, def_config, temp_db_curso # Each migration should come with two tests: # 1. Test that migration from old to new state works as expected. # 2. Test that the migration can be rerun on the new state without side effects. - - -@pytest.mark.parametrize('in_attr', ('', 'with time zone')) -def test_import_status_timestamp_change(temp_db_conn, temp_db_cursor, - table_factory, in_attr): - table_factory('import_status', - f"""lastimportdate timestamp {in_attr}, - sequence_id integer, - indexed boolean""") - - migration.import_status_timestamp_change(temp_db_conn) - temp_db_conn.commit() - - assert temp_db_cursor.scalar("""SELECT data_type FROM information_schema.columns - WHERE table_name = 'import_status' - and column_name = 'lastimportdate'""")\ - == 'timestamp with time zone' - - -def test_add_nominatim_property_table(temp_db_conn, temp_db_cursor, - def_config, monkeypatch): - # Use a r/o user name that always exists - monkeypatch.setenv('NOMINATIM_DATABASE_WEBUSER', 'postgres') - - assert not temp_db_cursor.table_exists('nominatim_properties') - - migration.add_nominatim_property_table(temp_db_conn, def_config) - temp_db_conn.commit() - - assert temp_db_cursor.table_exists('nominatim_properties') - - -def test_add_nominatim_property_table_repeat(temp_db_conn, temp_db_cursor, - def_config, property_table): - assert temp_db_cursor.table_exists('nominatim_properties') - - migration.add_nominatim_property_table(temp_db_conn, def_config) - temp_db_conn.commit() - - assert temp_db_cursor.table_exists('nominatim_properties') - - -def test_change_housenumber_transliteration(temp_db_conn, temp_db_cursor, - legacy_word_table, placex_table): - placex_table.add(housenumber='3A') - - temp_db_cursor.execute("""CREATE OR REPLACE FUNCTION make_standard_name(name TEXT) - RETURNS TEXT AS $$ SELECT lower(name) $$ LANGUAGE SQL """) - temp_db_cursor.execute("""CREATE OR REPLACE FUNCTION getorcreate_housenumber_id(lookup_word TEXT) - RETURNS INTEGER AS $$ SELECT 4325 $$ LANGUAGE SQL """) - - migration.change_housenumber_transliteration(temp_db_conn) - temp_db_conn.commit() - - assert temp_db_cursor.scalar('SELECT housenumber from placex') == '3a' - - migration.change_housenumber_transliteration(temp_db_conn) - temp_db_conn.commit() - - assert temp_db_cursor.scalar('SELECT housenumber from placex') == '3a' - - -def test_switch_placenode_geometry_index(temp_db_conn, temp_db_cursor, placex_table): - temp_db_cursor.execute("""CREATE INDEX idx_placex_adminname - ON placex (place_id)""") - - migration.switch_placenode_geometry_index(temp_db_conn) - temp_db_conn.commit() - - assert temp_db_cursor.index_exists('placex', 'idx_placex_geometry_placenode') - assert not temp_db_cursor.index_exists('placex', 'idx_placex_adminname') - - -def test_switch_placenode_geometry_index_repeat(temp_db_conn, temp_db_cursor, placex_table): - temp_db_cursor.execute("""CREATE INDEX idx_placex_geometry_placenode - ON placex (place_id)""") - - migration.switch_placenode_geometry_index(temp_db_conn) - temp_db_conn.commit() - - assert temp_db_cursor.index_exists('placex', 'idx_placex_geometry_placenode') - assert not temp_db_cursor.index_exists('placex', 'idx_placex_adminname') - assert temp_db_cursor.scalar("""SELECT indexdef from pg_indexes - WHERE tablename = 'placex' - and indexname = 'idx_placex_geometry_placenode' - """).endswith('(place_id)') - - -def test_install_legacy_tokenizer(temp_db_conn, temp_db_cursor, project_env, - property_table, table_factory, monkeypatch, - tmp_path): - table_factory('placex', 'place_id BIGINT') - table_factory('location_property_osmline', 'place_id BIGINT') - - # Setting up the tokenizer is problematic - class MiniTokenizer: - def migrate_database(self, config): - pass - - monkeypatch.setattr(migration.tokenizer_factory, 'create_tokenizer', - lambda cfg, **kwargs: MiniTokenizer()) - - migration.install_legacy_tokenizer(temp_db_conn, project_env) - temp_db_conn.commit() - - - -def test_install_legacy_tokenizer_repeat(temp_db_conn, temp_db_cursor, - def_config, property_table): - - property_table.set('tokenizer', 'dummy') - migration.install_legacy_tokenizer(temp_db_conn, def_config) - temp_db_conn.commit() - - -def test_create_tiger_housenumber_index(temp_db_conn, temp_db_cursor, table_factory): - table_factory('location_property_tiger', - 'parent_place_id BIGINT, startnumber INT, endnumber INT') - - migration.create_tiger_housenumber_index(temp_db_conn) - temp_db_conn.commit() - - if server_version_tuple(temp_db_conn) >= (11, 0, 0): - assert temp_db_cursor.index_exists('location_property_tiger', - 'idx_location_property_tiger_housenumber_migrated') - - migration.create_tiger_housenumber_index(temp_db_conn) - temp_db_conn.commit() diff --git a/test/python/tools/test_refresh_setup_website.py b/test/python/tools/test_refresh_setup_website.py deleted file mode 100644 index fe29dd52..00000000 --- a/test/python/tools/test_refresh_setup_website.py +++ /dev/null @@ -1,104 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# -# This file is part of Nominatim. (https://nominatim.org) -# -# Copyright (C) 2024 by the Nominatim developer community. -# For a full list of authors see the git log. -""" -Tests for setting up the website scripts. -""" -import subprocess - -import pytest - -from nominatim_db.tools import refresh - -@pytest.fixture -def test_script(tmp_path): - (tmp_path / 'php').mkdir() - - website_dir = (tmp_path / 'php' / 'website') - website_dir.mkdir() - - def _create_file(code): - outfile = website_dir / 'reverse-only-search.php' - outfile.write_text('