mirror of
https://github.com/osm-search/Nominatim.git
synced 2024-10-03 22:07:15 +03:00
commit
e92e03e2e6
93
.github/workflows/ci-tests.yml
vendored
93
.github/workflows/ci-tests.yml
vendored
@ -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
|
||||
|
@ -74,25 +74,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)
|
||||
#-----------------------------------------------------------------------------
|
||||
@ -125,8 +106,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}")
|
||||
@ -141,24 +120,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
|
||||
@ -203,11 +164,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}
|
||||
@ -243,10 +200,6 @@ if (BUILD_MODULE)
|
||||
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
|
||||
|
@ -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()
|
||||
|
@ -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()
|
@ -194,7 +194,7 @@ 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="<directory on the database server where nominatim.so resides>"
|
||||
```
|
||||
|
||||
|
@ -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
|
||||
<Directory "/srv/nominatim-project/website">
|
||||
Options FollowSymLinks MultiViews
|
||||
AddType text/html .php
|
||||
DirectoryIndex search.php
|
||||
Require all granted
|
||||
</Directory>
|
||||
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/<php version>/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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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).
|
||||
|
||||
|
@ -20,6 +20,18 @@ breaking changes. **Please read them before running the migration.**
|
||||
If you are migrating from a version <3.6, then you still have to follow
|
||||
the manual migration steps up to 3.6.
|
||||
|
||||
## 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
|
||||
|
@ -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
|
||||
|
@ -168,7 +168,7 @@ Additional information requested with `addressdetails=1`, `extratags=1` and
|
||||
<searchresults timestamp="Sat, 11 Aug 18 11:55:35 +0000"
|
||||
attribution="Data © OpenStreetMap contributors, ODbL 1.0. https://www.openstreetmap.org/copyright"
|
||||
querystring="london" polygon="false" exclude_place_ids="100149"
|
||||
more_url="https://nominatim.openstreetmap.org/search.php?q=london&addressdetails=1&extratags=1&exclude_place_ids=100149&format=xml&accept-language=en-US%2Cen%3Bq%3D0.7%2Cde%3Bq%3D0.3">
|
||||
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">
|
||||
<place place_id="100149" osm_type="node" osm_id="107775" place_rank="15" address_rank="15"
|
||||
boundingbox="51.3473219,51.6673219,-0.2876474,0.0323526" lat="51.5073219" lon="-0.1276474"
|
||||
display_name="London, Greater London, England, SW1A 2DU, United Kingdom"
|
||||
|
@ -1,12 +1,3 @@
|
||||
!!! Attention
|
||||
The current version of Nominatim implements two different search frontends:
|
||||
the old PHP frontend and the new Python frontend. They have a very similar
|
||||
API but differ in some implementation details. These are marked in the
|
||||
documentation as `[Python-only]` or `[PHP-only]`.
|
||||
|
||||
`https://nominatim.openstreetmap.org` implements the **Python frontend**.
|
||||
So users should refer to the **`[Python-only]`** comments.
|
||||
|
||||
This section describes the API V1 of the Nominatim web service. The
|
||||
service offers the following endpoints:
|
||||
|
||||
|
@ -32,10 +32,9 @@ projection. The API returns exactly one result or an error when the coordinate
|
||||
is in an area with no OSM data coverage.
|
||||
|
||||
|
||||
!!! danger "Deprecation warning"
|
||||
The reverse API used to allow address lookup for a single OSM object by
|
||||
its OSM id for `[PHP-only]`. The use is considered deprecated.
|
||||
Use the [Address Lookup API](Lookup.md) instead.
|
||||
!!! tip
|
||||
The reverse API allows a lookup of object by coordinate. If you want
|
||||
to look up an object by ID, use the [Address Lookup API](Lookup.md) instead.
|
||||
|
||||
!!! danger "Deprecation warning"
|
||||
The API can also be used with the URL
|
||||
@ -149,8 +148,6 @@ In terms of address details the zoom levels are as follows:
|
||||
|-----------| ----- | ------- |
|
||||
| layer | comma-separated list of: `address`, `poi`, `railway`, `natural`, `manmade` | _unset_ (no restriction) |
|
||||
|
||||
**`[Python-only]`**
|
||||
|
||||
The layer filter allows to select places by themes.
|
||||
|
||||
The `address` layer contains all places that make up an address:
|
||||
|
@ -187,8 +187,6 @@ also excluded when the filter is set.
|
||||
|-----------| ----- | ------- |
|
||||
| layer | comma-separated list of: `address`, `poi`, `railway`, `natural`, `manmade` | _unset_ (no restriction) |
|
||||
|
||||
**`[Python-only]`**
|
||||
|
||||
The layer filter allows to select places by themes.
|
||||
|
||||
The `address` layer contains all places that make up an address:
|
||||
|
@ -57,8 +57,7 @@ parameter that is understood by libpq. See the [Postgres documentation](https://
|
||||
| **After Changes:** | cannot be changed after import |
|
||||
|
||||
Defines the name of the database user that will run search queries. Usually
|
||||
this is the user under which the webserver is executed. When running Nominatim
|
||||
via php-fpm, you can also define a separate query user. The Postgres user
|
||||
this is the user under which the webserver is executed. The Postgres user
|
||||
needs to be set up before starting the import.
|
||||
|
||||
Nominatim grants minimal rights to this user to all tables that are needed
|
||||
@ -544,38 +543,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 +583,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 +694,8 @@ The entries in the log file have the following format:
|
||||
<request time> <execution time in s> <number of results> <type> "<query string>"
|
||||
|
||||
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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
@ -118,7 +101,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`)
|
||||
|
@ -91,14 +91,9 @@ for a custom tokenizer implementation.
|
||||
|
||||
### Directory Structure
|
||||
|
||||
Nominatim expects two files for a tokenizer:
|
||||
|
||||
* `nominatim/tokenizer/<NAME>_tokenizer.py` containing the Python part of the
|
||||
implementation
|
||||
* `lib-php/tokenizer/<NAME>_tokenizer.php` with the PHP part of the
|
||||
implementation
|
||||
|
||||
where `<NAME>` is a unique name for the tokenizer consisting of only lower-case
|
||||
Nominatim expects a single file `src/nominatim_db/tokenizer/<NAME>_tokenizer.py`
|
||||
containing the Python part of the implementation.
|
||||
`<NAME>` 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.
|
||||
|
||||
|
@ -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`.
|
||||
|
@ -1,191 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
require_once(CONST_LibDir.'/ClassTypes.php');
|
||||
|
||||
/**
|
||||
* Detailed list of address parts for a single result
|
||||
*/
|
||||
class AddressDetails
|
||||
{
|
||||
private $iPlaceID;
|
||||
private $aAddressLines;
|
||||
|
||||
public function __construct(&$oDB, $iPlaceID, $sHousenumber, $mLangPref)
|
||||
{
|
||||
$this->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'];
|
||||
}
|
||||
}
|
||||
}
|
@ -1,576 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim\ClassTypes;
|
||||
|
||||
/**
|
||||
* Create a label tag for the given place that can be used as an XML name.
|
||||
*
|
||||
* @param array[] $aPlace Information about the place to label.
|
||||
*
|
||||
* A label tag groups various object types together under a common
|
||||
* label. The returned value is lower case and has no spaces
|
||||
*/
|
||||
function getLabelTag($aPlace, $sCountry = null)
|
||||
{
|
||||
$iRank = (int) ($aPlace['rank_address'] ?? 30);
|
||||
$sLabel;
|
||||
if (isset($aPlace['place_type'])) {
|
||||
$sLabel = $aPlace['place_type'];
|
||||
} elseif ($aPlace['class'] == 'boundary' && $aPlace['type'] == 'administrative') {
|
||||
$sLabel = getBoundaryLabel($iRank/2, $sCountry);
|
||||
} elseif ($aPlace['type'] == 'postal_code') {
|
||||
$sLabel = 'postcode';
|
||||
} elseif ($iRank < 26) {
|
||||
$sLabel = $aPlace['type'];
|
||||
} elseif ($iRank < 28) {
|
||||
$sLabel = 'road';
|
||||
} elseif ($aPlace['class'] == 'place'
|
||||
&& ($aPlace['type'] == 'house_number' ||
|
||||
$aPlace['type'] == 'house_name' ||
|
||||
$aPlace['type'] == 'country_code')
|
||||
) {
|
||||
$sLabel = $aPlace['type'];
|
||||
} else {
|
||||
$sLabel = $aPlace['class'];
|
||||
}
|
||||
|
||||
return strtolower(str_replace(' ', '_', $sLabel));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a label for the given place.
|
||||
*
|
||||
* @param array[] $aPlace Information about the place to label.
|
||||
*/
|
||||
function getLabel($aPlace, $sCountry = null)
|
||||
{
|
||||
if (isset($aPlace['place_type'])) {
|
||||
return ucwords(str_replace('_', ' ', $aPlace['place_type']));
|
||||
}
|
||||
|
||||
if ($aPlace['class'] == 'boundary' && $aPlace['type'] == 'administrative') {
|
||||
return getBoundaryLabel(($aPlace['rank_address'] ?? 30)/2, $sCountry ?? null);
|
||||
}
|
||||
|
||||
// Return a label only for 'important' class/type combinations
|
||||
if (getImportance($aPlace) !== null) {
|
||||
return ucwords(str_replace('_', ' ', $aPlace['type']));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a simple label for an administrative boundary for the given country.
|
||||
*
|
||||
* @param int $iAdminLevel Content of admin_level tag.
|
||||
* @param string $sCountry Country code of the country where the object is
|
||||
* in. May be null, in which case a world-wide
|
||||
* fallback is used.
|
||||
* @param string $sFallback String to return if no explicit string is listed.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getBoundaryLabel($iAdminLevel, $sCountry, $sFallback = 'Administrative')
|
||||
{
|
||||
static $aBoundaryList = array (
|
||||
'default' => 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;
|
||||
}
|
362
lib-php/DB.php
362
lib-php/DB.php
@ -1,362 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
require_once(CONST_LibDir.'/DatabaseError.php');
|
||||
|
||||
/**
|
||||
* Uses PDO to access the database specified in the CONST_Database_DSN
|
||||
* setting.
|
||||
*/
|
||||
class DB
|
||||
{
|
||||
protected $connection;
|
||||
|
||||
public function __construct($sDSN = null)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
class DatabaseError extends \Exception
|
||||
{
|
||||
|
||||
public function __construct($message, $code, $previous, $oPDOErr, $sSql = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
// https://secure.php.net/manual/en/class.pdoexception.php
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,189 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
class Debug
|
||||
{
|
||||
public static function newFunction($sHeading)
|
||||
{
|
||||
echo "<pre><h2>Debug output for $sHeading</h2></pre>\n";
|
||||
}
|
||||
|
||||
public static function newSection($sHeading)
|
||||
{
|
||||
echo "<hr><pre><h3>$sHeading</h3></pre>\n";
|
||||
}
|
||||
|
||||
public static function printVar($sHeading, $mVar)
|
||||
{
|
||||
echo '<pre><b>'.$sHeading. ':</b> ';
|
||||
Debug::outputVar($mVar, str_repeat(' ', strlen($sHeading) + 3));
|
||||
echo "</pre>\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 '<b>'.$sHeading.":</b>\n";
|
||||
echo "<table border='1'>\n";
|
||||
if (!empty($aVar)) {
|
||||
echo " <tr>\n";
|
||||
$aKeys = array();
|
||||
$aInfo = reset($aVar);
|
||||
if (!is_array($aInfo)) {
|
||||
$aInfo = $aInfo->debugInfo();
|
||||
}
|
||||
foreach ($aInfo as $sKey => $mVal) {
|
||||
echo ' <th><small>'.$sKey.'</small></th>'."\n";
|
||||
$aKeys[] = $sKey;
|
||||
}
|
||||
echo " </tr>\n";
|
||||
foreach ($aVar as $oRow) {
|
||||
$aInfo = $oRow;
|
||||
if (!is_array($oRow)) {
|
||||
$aInfo = $oRow->debugInfo();
|
||||
}
|
||||
echo " <tr>\n";
|
||||
foreach ($aKeys as $sKey) {
|
||||
echo ' <td><pre>';
|
||||
if (isset($aInfo[$sKey])) {
|
||||
Debug::outputVar($aInfo[$sKey], '');
|
||||
}
|
||||
echo '</pre></td>'."\n";
|
||||
}
|
||||
echo " </tr>\n";
|
||||
}
|
||||
}
|
||||
echo "</table>\n";
|
||||
}
|
||||
|
||||
public static function printGroupedSearch($aSearches, $aWordsIDs)
|
||||
{
|
||||
echo '<table border="1">';
|
||||
echo '<tr><th>rank</th><th>Name Tokens</th><th>Name Not</th>';
|
||||
echo '<th>Address Tokens</th><th>Address Not</th>';
|
||||
echo '<th>country</th><th>operator</th>';
|
||||
echo '<th>class</th><th>type</th><th>postcode</th><th>housenumber</th></tr>';
|
||||
foreach ($aSearches as $aRankedSet) {
|
||||
foreach ($aRankedSet as $aRow) {
|
||||
$aRow->dumpAsHtmlTableRow($aWordsIDs);
|
||||
}
|
||||
}
|
||||
echo '</table>';
|
||||
}
|
||||
|
||||
public static function printGroupTable($sHeading, $aVar)
|
||||
{
|
||||
echo '<b>'.$sHeading.":</b>\n";
|
||||
echo "<table border='1'>\n";
|
||||
if (!empty($aVar)) {
|
||||
echo " <tr>\n";
|
||||
echo ' <th><small>Group</small></th>'."\n";
|
||||
$aKeys = array();
|
||||
$aInfo = reset($aVar)[0];
|
||||
if (!is_array($aInfo)) {
|
||||
$aInfo = $aInfo->debugInfo();
|
||||
}
|
||||
foreach ($aInfo as $sKey => $mVal) {
|
||||
echo ' <th><small>'.$sKey.'</small></th>'."\n";
|
||||
$aKeys[] = $sKey;
|
||||
}
|
||||
echo " </tr>\n";
|
||||
foreach ($aVar as $sGrpKey => $aGroup) {
|
||||
foreach ($aGroup as $oRow) {
|
||||
$aInfo = $oRow;
|
||||
if (!is_array($oRow)) {
|
||||
$aInfo = $oRow->debugInfo();
|
||||
}
|
||||
echo " <tr>\n";
|
||||
echo ' <td><pre>'.$sGrpKey.'</pre></td>'."\n";
|
||||
foreach ($aKeys as $sKey) {
|
||||
echo ' <td><pre>';
|
||||
if (!empty($aInfo[$sKey])) {
|
||||
Debug::outputVar($aInfo[$sKey], '');
|
||||
}
|
||||
echo '</pre></td>'."\n";
|
||||
}
|
||||
echo " </tr>\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
echo "</table>\n";
|
||||
}
|
||||
|
||||
public static function printSQL($sSQL)
|
||||
{
|
||||
echo '<p><tt><b>'.date('c').'</b> <font color="#aaa">'.htmlspecialchars($sSQL, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401).'</font></tt></p>'."\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 '<i>'.($mVar ? 'True' : 'False').'</i>';
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
class Debug
|
||||
{
|
||||
public static function __callStatic($name, $arguments)
|
||||
{
|
||||
// nothing
|
||||
}
|
||||
}
|
@ -1,938 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
require_once(CONST_LibDir.'/PlaceLookup.php');
|
||||
require_once(CONST_LibDir.'/Phrase.php');
|
||||
require_once(CONST_LibDir.'/ReverseGeocode.php');
|
||||
require_once(CONST_LibDir.'/SearchDescription.php');
|
||||
require_once(CONST_LibDir.'/SearchContext.php');
|
||||
require_once(CONST_LibDir.'/SearchPosition.php');
|
||||
require_once(CONST_LibDir.'/TokenList.php');
|
||||
require_once(CONST_TokenizerDir.'/tokenizer.php');
|
||||
|
||||
class Geocode
|
||||
{
|
||||
protected $oDB;
|
||||
|
||||
protected $oPlaceLookup;
|
||||
protected $oTokenizer;
|
||||
|
||||
protected $aLangPrefOrder = array();
|
||||
|
||||
protected $aExcludePlaceIDs = array();
|
||||
|
||||
protected $iLimit = 20;
|
||||
protected $iFinalLimit = 10;
|
||||
protected $iOffset = 0;
|
||||
protected $bFallback = false;
|
||||
|
||||
protected $aCountryCodes = false;
|
||||
|
||||
protected $bBoundedSearch = false;
|
||||
protected $aViewBox = false;
|
||||
protected $aRoutePoints = false;
|
||||
protected $aRouteWidth = false;
|
||||
|
||||
protected $iMaxRank = 20;
|
||||
protected $iMinAddressRank = 0;
|
||||
protected $iMaxAddressRank = 30;
|
||||
protected $aAddressRankList = array();
|
||||
|
||||
protected $sAllowedTypesSQLList = false;
|
||||
|
||||
protected $sQuery = false;
|
||||
protected $aStructuredQuery = false;
|
||||
|
||||
|
||||
public function __construct(&$oDB)
|
||||
{
|
||||
$this->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
|
||||
$sQuery = $oParams->getString('q');
|
||||
if (!$sQuery) {
|
||||
$this->setStructuredQuery(
|
||||
$oParams->getString('amenity'),
|
||||
$oParams->getString('street'),
|
||||
$oParams->getString('city'),
|
||||
$oParams->getString('county'),
|
||||
$oParams->getString('state'),
|
||||
$oParams->getString('country'),
|
||||
$oParams->getString('postalcode')
|
||||
);
|
||||
} else {
|
||||
$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;
|
||||
|
||||
// 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 > 20) {
|
||||
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 > 4 || $iQueryLoop > 30) {
|
||||
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
|
@ -1,157 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
class ParameterParser
|
||||
{
|
||||
private $aParams;
|
||||
|
||||
|
||||
public function __construct($aParams = null)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
/**
|
||||
* Segment of a query string.
|
||||
*
|
||||
* The parts of a query strings are usually separated by commas.
|
||||
*/
|
||||
class Phrase
|
||||
{
|
||||
// Complete phrase as a string (guaranteed to have no leading or trailing
|
||||
// spaces).
|
||||
private $sPhrase;
|
||||
// Element type for structured searches.
|
||||
private $sPhraseType;
|
||||
// Possible segmentations of the phrase.
|
||||
private $aWordSets;
|
||||
|
||||
public function __construct($sPhrase, $sPhraseType)
|
||||
{
|
||||
$this->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
|
||||
);
|
||||
}
|
||||
}
|
@ -1,615 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
require_once(CONST_LibDir.'/AddressDetails.php');
|
||||
require_once(CONST_LibDir.'/Result.php');
|
||||
|
||||
class PlaceLookup
|
||||
{
|
||||
protected $oDB;
|
||||
|
||||
protected $aLangPrefOrderSql = "''";
|
||||
|
||||
protected $bAddressDetails = false;
|
||||
protected $bExtraTags = false;
|
||||
protected $bNameDetails = false;
|
||||
|
||||
protected $bIncludePolygonAsText = false;
|
||||
protected $bIncludePolygonAsGeoJSON = false;
|
||||
protected $bIncludePolygonAsKML = false;
|
||||
protected $bIncludePolygonAsSVG = false;
|
||||
protected $fPolygonSimplificationThreshold = 0.0;
|
||||
|
||||
protected $sAnchorSql = null;
|
||||
protected $sAddressRankListSql = null;
|
||||
protected $sAllowedTypesSQLList = null;
|
||||
protected $bDeDupe = true;
|
||||
|
||||
|
||||
public function __construct(&$oDB)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
/**
|
||||
* A single result of a search operation or a reverse lookup.
|
||||
*
|
||||
* This object only contains the id of the result. It does not yet
|
||||
* have any details needed to format the output document.
|
||||
*/
|
||||
class Result
|
||||
{
|
||||
const TABLE_PLACEX = 0;
|
||||
const TABLE_POSTCODE = 1;
|
||||
const TABLE_OSMLINE = 2;
|
||||
const TABLE_TIGER = 3;
|
||||
|
||||
/// Database table that contains the result.
|
||||
public $iTable;
|
||||
/// Id of the result.
|
||||
public $iId;
|
||||
/// House number (only for interpolation results).
|
||||
public $iHouseNumber = -1;
|
||||
/// Number of exact matches in address (address searches only).
|
||||
public $iExactMatches = 0;
|
||||
/// Subranking within the results (the higher the worse).
|
||||
public $iResultRank = 0;
|
||||
/// Address rank of the result.
|
||||
public $iAddressRank;
|
||||
|
||||
public function debugInfo()
|
||||
{
|
||||
return array(
|
||||
'Table' => $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);
|
||||
}
|
||||
}
|
@ -1,401 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
require_once(CONST_LibDir.'/Result.php');
|
||||
|
||||
class ReverseGeocode
|
||||
{
|
||||
protected $oDB;
|
||||
protected $iMaxRank = 28;
|
||||
|
||||
|
||||
public function __construct(&$oDB)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
@ -1,319 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
require_once(CONST_LibDir.'/lib.php');
|
||||
|
||||
|
||||
/**
|
||||
* Collection of search constraints that are independent of the
|
||||
* actual interpretation of the search query.
|
||||
*
|
||||
* The search context is shared between all SearchDescriptions. This
|
||||
* object mainly serves as context provider for the database queries.
|
||||
* Therefore most data is directly cached as SQL statements.
|
||||
*/
|
||||
class SearchContext
|
||||
{
|
||||
/// Search radius around a given Near reference point.
|
||||
private $fNearRadius = false;
|
||||
/// True if search must be restricted to viewbox only.
|
||||
public $bViewboxBounded = false;
|
||||
|
||||
/// Reference point for search (as SQL).
|
||||
public $sqlNear = '';
|
||||
/// Viewbox selected for search (as SQL).
|
||||
public $sqlViewboxSmall = '';
|
||||
/// Viewbox with a larger buffer around (as SQL).
|
||||
public $sqlViewboxLarge = '';
|
||||
/// Reference along a route (as SQL).
|
||||
public $sqlViewboxCentre = '';
|
||||
/// List of countries to restrict search to (as array).
|
||||
public $aCountryList = null;
|
||||
/// List of countries to restrict search to (as SQL).
|
||||
public $sqlCountryList = '';
|
||||
/// List of place IDs to exclude (as SQL).
|
||||
private $sqlExcludeList = '';
|
||||
/// Subset of word ids of full words in the query.
|
||||
private $aFullNameWords = array();
|
||||
|
||||
public function setFullNameWords($aWordList)
|
||||
{
|
||||
$this->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
|
||||
);
|
||||
}
|
||||
}
|
@ -1,985 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
require_once(CONST_LibDir.'/SpecialSearchOperator.php');
|
||||
require_once(CONST_LibDir.'/SearchContext.php');
|
||||
require_once(CONST_LibDir.'/Result.php');
|
||||
|
||||
/**
|
||||
* Description of a single interpretation of a search query.
|
||||
*/
|
||||
class SearchDescription
|
||||
{
|
||||
/// Ranking how well the description fits the query.
|
||||
private $iSearchRank = 0;
|
||||
/// Country code of country the result must belong to.
|
||||
private $sCountryCode = '';
|
||||
/// List of word ids making up the name of the object.
|
||||
private $aName = array();
|
||||
/// True if the name is rare enough to force index use on name.
|
||||
private $bRareName = false;
|
||||
/// True if the name requires to be accompanied by address terms.
|
||||
private $bNameNeedsAddress = false;
|
||||
/// List of word ids making up the address of the object.
|
||||
private $aAddress = array();
|
||||
/// List of word ids that appear in the name but should be ignored.
|
||||
private $aNameNonSearch = array();
|
||||
/// List of word ids that appear in the address but should be ignored.
|
||||
private $aAddressNonSearch = array();
|
||||
/// Kind of search for special searches, see Nominatim::Operator.
|
||||
private $iOperator = Operator::NONE;
|
||||
/// Class of special feature to search for.
|
||||
private $sClass = '';
|
||||
/// Type of special feature to search for.
|
||||
private $sType = '';
|
||||
/// Housenumber of the object.
|
||||
private $sHouseNumber = '';
|
||||
/// Postcode for the object.
|
||||
private $sPostcode = '';
|
||||
/// Global search constraints.
|
||||
private $oContext;
|
||||
|
||||
// Temporary values used while creating the search description.
|
||||
|
||||
/// Index of phrase currently processed.
|
||||
private $iNamePhrase = -1;
|
||||
|
||||
/**
|
||||
* Create an empty search description.
|
||||
*
|
||||
* @param object $oContext Global context to use. Will be inherited by
|
||||
* all derived search objects.
|
||||
*/
|
||||
public function __construct($oContext)
|
||||
{
|
||||
$this->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 [<key>=<value>].
|
||||
* 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 [<key>=<value>].
|
||||
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 '<tr>';
|
||||
echo "<td>$this->iSearchRank</td>";
|
||||
echo '<td>'.join(', ', array_map($kf, $this->aName)).'</td>';
|
||||
echo '<td>'.join(', ', array_map($kf, $this->aNameNonSearch)).'</td>';
|
||||
echo '<td>'.join(', ', array_map($kf, $this->aAddress)).'</td>';
|
||||
echo '<td>'.join(', ', array_map($kf, $this->aAddressNonSearch)).'</td>';
|
||||
echo '<td>'.$this->sCountryCode.'</td>';
|
||||
echo '<td>'.Operator::toString($this->iOperator).'</td>';
|
||||
echo '<td>'.$this->sClass.'</td>';
|
||||
echo '<td>'.$this->sType.'</td>';
|
||||
echo '<td>'.$this->sPostcode.'</td>';
|
||||
echo '<td>'.$this->sHouseNumber.'</td>';
|
||||
|
||||
echo '</tr>';
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
/**
|
||||
* Description of the position of a token within a query.
|
||||
*/
|
||||
class SearchPosition
|
||||
{
|
||||
private $sPhraseType;
|
||||
|
||||
private $iPhrase;
|
||||
private $iNumPhrases;
|
||||
|
||||
private $iToken;
|
||||
private $iNumTokens;
|
||||
|
||||
|
||||
public function __construct($sPhraseType, $iPhrase, $iNumPhrases)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
class Shell
|
||||
{
|
||||
public function __construct($sBaseCmd, ...$aParams)
|
||||
{
|
||||
if (!$sBaseCmd) {
|
||||
throw new \Exception('Command missing in new() call');
|
||||
}
|
||||
$this->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);
|
||||
}
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
/**
|
||||
* A word list creator based on simple splitting by space.
|
||||
*
|
||||
* Creates possible permutations of split phrases by finding all combination
|
||||
* of splitting the phrase on space boundaries.
|
||||
*/
|
||||
class SimpleWordList
|
||||
{
|
||||
const MAX_WORDSET_LEN = 20;
|
||||
const MAX_WORDSETS = 100;
|
||||
|
||||
// The phrase as a list of simple terms (without spaces).
|
||||
private $aWords;
|
||||
|
||||
/**
|
||||
* Create a new word list
|
||||
*
|
||||
* @param string sPhrase Phrase to create the word list from. The phrase is
|
||||
* expected to be normalised, so that there are no
|
||||
* subsequent spaces.
|
||||
*/
|
||||
public function __construct($sPhrase)
|
||||
{
|
||||
if (strlen($sPhrase) > 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;
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
/**
|
||||
* Operators describing special searches.
|
||||
*/
|
||||
abstract class Operator
|
||||
{
|
||||
/// No operator selected.
|
||||
const NONE = 0;
|
||||
/// Search for POI of the given type.
|
||||
const TYPE = 1;
|
||||
/// Search for POIs near the given place.
|
||||
const NEAR = 2;
|
||||
/// Search for POIS in the given place.
|
||||
const IN = 3;
|
||||
/// Search for POIS named as given.
|
||||
const NAME = 4;
|
||||
/// Search for postcodes.
|
||||
const POSTCODE = 5;
|
||||
|
||||
private static $aConstantNames = null;
|
||||
|
||||
|
||||
public static function toString($iOperator)
|
||||
{
|
||||
if ($iOperator == Operator::NONE) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (Operator::$aConstantNames === null) {
|
||||
$oReflector = new \ReflectionClass('Nominatim\Operator');
|
||||
$aConstants = $oReflector->getConstants();
|
||||
|
||||
Operator::$aConstantNames = array();
|
||||
foreach ($aConstants as $sName => $iValue) {
|
||||
Operator::$aConstantNames[$iValue] = $sName;
|
||||
}
|
||||
}
|
||||
|
||||
return Operator::$aConstantNames[$iOperator];
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
require_once(CONST_TokenizerDir.'/tokenizer.php');
|
||||
|
||||
use Exception;
|
||||
|
||||
class Status
|
||||
{
|
||||
protected $oDB;
|
||||
|
||||
public function __construct(&$oDB)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim\Token;
|
||||
|
||||
/**
|
||||
* A country token.
|
||||
*/
|
||||
class Country
|
||||
{
|
||||
/// Database word id, if available.
|
||||
private $iId;
|
||||
/// Two-letter country code (lower-cased).
|
||||
private $sCountryCode;
|
||||
|
||||
public function __construct($iId, $sCountryCode)
|
||||
{
|
||||
$this->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';
|
||||
}
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim\Token;
|
||||
|
||||
/**
|
||||
* A house number token.
|
||||
*/
|
||||
class HouseNumber
|
||||
{
|
||||
/// Database word id, if available.
|
||||
private $iId;
|
||||
/// Normalized house number.
|
||||
private $sToken;
|
||||
|
||||
public function __construct($iId, $sToken)
|
||||
{
|
||||
$this->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';
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
require_once(CONST_LibDir.'/TokenCountry.php');
|
||||
require_once(CONST_LibDir.'/TokenHousenumber.php');
|
||||
require_once(CONST_LibDir.'/TokenPostcode.php');
|
||||
require_once(CONST_LibDir.'/TokenSpecialTerm.php');
|
||||
require_once(CONST_LibDir.'/TokenWord.php');
|
||||
require_once(CONST_LibDir.'/TokenPartial.php');
|
||||
require_once(CONST_LibDir.'/SpecialSearchOperator.php');
|
||||
|
||||
/**
|
||||
* Saves information about the tokens that appear in a search query.
|
||||
*
|
||||
* Tokens are sorted by their normalized form, the token word. There are different
|
||||
* kinds of tokens, represented by different Token* classes. Note that
|
||||
* tokens do not have a common base class. All tokens need to have a field
|
||||
* with the word id that points to an entry in the `word` database table
|
||||
* but otherwise the information saved about a token can be very different.
|
||||
*/
|
||||
class TokenList
|
||||
{
|
||||
// List of list of tokens indexed by their word_token.
|
||||
private $aTokens = array();
|
||||
|
||||
|
||||
/**
|
||||
* Return total number of tokens.
|
||||
*
|
||||
* @return Integer
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return count($this->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;
|
||||
}
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim\Token;
|
||||
|
||||
/**
|
||||
* A standard word token.
|
||||
*/
|
||||
class Partial
|
||||
{
|
||||
/// Database word id, if applicable.
|
||||
private $iId;
|
||||
/// Number of appearances in the database.
|
||||
private $iSearchNameCount;
|
||||
/// True, if the token consists exclusively of digits and spaces.
|
||||
private $bNumberToken;
|
||||
|
||||
public function __construct($iId, $sToken, $iSearchNameCount)
|
||||
{
|
||||
$this->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';
|
||||
}
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim\Token;
|
||||
|
||||
/**
|
||||
* A postcode token.
|
||||
*/
|
||||
class Postcode
|
||||
{
|
||||
/// Database word id, if available.
|
||||
private $iId;
|
||||
/// Full normalized postcode (upper cased).
|
||||
private $sPostcode;
|
||||
// Optional country code the postcode belongs to (currently unused).
|
||||
private $sCountryCode;
|
||||
|
||||
public function __construct($iId, $sPostcode, $sCountryCode = '')
|
||||
{
|
||||
$this->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';
|
||||
}
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim\Token;
|
||||
|
||||
require_once(CONST_LibDir.'/SpecialSearchOperator.php');
|
||||
|
||||
/**
|
||||
* A word token describing a place type.
|
||||
*/
|
||||
class SpecialTerm
|
||||
{
|
||||
/// Database word id, if applicable.
|
||||
private $iId;
|
||||
/// Class (or OSM tag key) of the place to look for.
|
||||
private $sClass;
|
||||
/// Type (or OSM tag value) of the place to look for.
|
||||
private $sType;
|
||||
/// Relationship of the operator to the object (see Operator class).
|
||||
private $iOperator;
|
||||
|
||||
public function __construct($iID, $sClass, $sType, $iOperator)
|
||||
{
|
||||
$this->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';
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim\Token;
|
||||
|
||||
/**
|
||||
* A standard word token.
|
||||
*/
|
||||
class Word
|
||||
{
|
||||
/// Database word id, if applicable.
|
||||
private $iId;
|
||||
/// Number of appearances in the database.
|
||||
private $iSearchNameCount;
|
||||
/// Number of terms in the word.
|
||||
private $iTermCount;
|
||||
|
||||
public function __construct($iId, $iSearchNameCount, $iTermCount)
|
||||
{
|
||||
$this->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';
|
||||
}
|
||||
}
|
199
lib-php/cmd.php
199
lib-php/cmd.php
@ -1,199 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
require_once(CONST_LibDir.'/Shell.php');
|
||||
|
||||
function getCmdOpt($aArg, $aSpec, &$aResult, $bExitOnError = false, $bExitOnUnknown = false)
|
||||
{
|
||||
$aQuick = array();
|
||||
$aCounts = array();
|
||||
|
||||
foreach ($aSpec as $aLine) {
|
||||
if (is_array($aLine)) {
|
||||
if ($aLine[0]) {
|
||||
$aQuick['--'.$aLine[0]] = $aLine;
|
||||
}
|
||||
if ($aLine[1]) {
|
||||
$aQuick['-'.$aLine[1]] = $aLine;
|
||||
}
|
||||
$aCounts[$aLine[0]] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$aResult = array();
|
||||
$bUnknown = false;
|
||||
$iSize = count($aArg);
|
||||
for ($i = 1; $i < $iSize; $i++) {
|
||||
if (isset($aQuick[$aArg[$i]])) {
|
||||
$aLine = $aQuick[$aArg[$i]];
|
||||
$aCounts[$aLine[0]]++;
|
||||
$xVal = null;
|
||||
if ($aLine[4] == $aLine[5]) {
|
||||
if ($aLine[4]) {
|
||||
$xVal = array();
|
||||
for ($n = $aLine[4]; $i < $iSize && $n; $n--) {
|
||||
$i++;
|
||||
if ($i >= $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);
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
require('Symfony/Component/Dotenv/autoload.php');
|
||||
|
||||
function loadDotEnv()
|
||||
{
|
||||
$dotenv = new \Symfony\Component\Dotenv\Dotenv();
|
||||
$dotenv->load(CONST_ConfigDir.'/env.defaults');
|
||||
|
||||
if (file_exists('.env')) {
|
||||
$dotenv->load('.env');
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
require_once('init.php');
|
||||
require_once('cmd.php');
|
||||
require_once('DebugNone.php');
|
@ -1,98 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
require_once('init.php');
|
||||
require_once('ParameterParser.php');
|
||||
require_once(CONST_Debug ? 'DebugHtml.php' : 'DebugNone.php');
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
* Error handling functions
|
||||
*
|
||||
*/
|
||||
|
||||
function userError($sMsg)
|
||||
{
|
||||
throw new \Exception($sMsg, 400);
|
||||
}
|
||||
|
||||
|
||||
function exception_handler_json($exception)
|
||||
{
|
||||
http_response_code($exception->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 '<?xml version="1.0" encoding="UTF-8" ?>'."\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');
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
require_once(CONST_LibDir.'/lib.php');
|
||||
require_once(CONST_LibDir.'/DB.php');
|
246
lib-php/lib.php
246
lib-php/lib.php
@ -1,246 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
function loadSettings($sProjectDir)
|
||||
{
|
||||
@define('CONST_InstallDir', $sProjectDir);
|
||||
// Temporary hack to set the directory via environment instead of
|
||||
// the installed scripts. Neither setting is part of the official
|
||||
// set of settings.
|
||||
defined('CONST_ConfigDir') or define('CONST_ConfigDir', $_SERVER['NOMINATIM_CONFIGDIR']);
|
||||
}
|
||||
|
||||
function getSetting($sConfName, $sDefault = null)
|
||||
{
|
||||
$sValue = $_SERVER['NOMINATIM_'.$sConfName];
|
||||
|
||||
if ($sDefault !== null && !$sValue) {
|
||||
return $sDefault;
|
||||
}
|
||||
|
||||
return $sValue;
|
||||
}
|
||||
|
||||
function getSettingBool($sConfName)
|
||||
{
|
||||
$sVal = strtolower(getSetting($sConfName));
|
||||
|
||||
return strcmp($sVal, 'yes') == 0
|
||||
|| strcmp($sVal, 'true') == 0
|
||||
|| strcmp($sVal, '1') == 0;
|
||||
}
|
||||
|
||||
function fail($sError, $sUserError = false)
|
||||
{
|
||||
if (!$sUserError) {
|
||||
$sUserError = $sError;
|
||||
}
|
||||
error_log('ERROR: '.$sError);
|
||||
var_dump($sUserError);
|
||||
echo "\n";
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
|
||||
function getProcessorCount()
|
||||
{
|
||||
$sCPU = file_get_contents('/proc/cpuinfo');
|
||||
preg_match_all('#processor\s+: [0-9]+#', $sCPU, $aMatches);
|
||||
return count($aMatches[0]);
|
||||
}
|
||||
|
||||
|
||||
function getTotalMemoryMB()
|
||||
{
|
||||
$sCPU = file_get_contents('/proc/meminfo');
|
||||
preg_match('#MemTotal: +([0-9]+) kB#', $sCPU, $aMatches);
|
||||
return (int)($aMatches[1]/1024);
|
||||
}
|
||||
|
||||
|
||||
function getCacheMemoryMB()
|
||||
{
|
||||
$sCPU = file_get_contents('/proc/meminfo');
|
||||
preg_match('#Cached: +([0-9]+) kB#', $sCPU, $aMatches);
|
||||
return (int)($aMatches[1]/1024);
|
||||
}
|
||||
|
||||
function getDatabaseDate(&$oDB)
|
||||
{
|
||||
// Find the newest node in the DB
|
||||
$iLastOSMID = $oDB->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));
|
||||
}
|
||||
}
|
||||
}
|
104
lib-php/log.php
104
lib-php/log.php
@ -1,104 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
function logStart(&$oDB, $sType = '', $sQuery = '', $aLanguageList = array())
|
||||
{
|
||||
$fStartTime = microtime(true);
|
||||
$aStartTime = explode('.', $fStartTime);
|
||||
if (!isset($aStartTime[1])) {
|
||||
$aStartTime[1] = '0';
|
||||
}
|
||||
|
||||
$sOutputFormat = '';
|
||||
if (isset($_GET['format'])) {
|
||||
$sOutputFormat = $_GET['format'];
|
||||
}
|
||||
|
||||
if ($sType == 'reverse') {
|
||||
$sOutQuery = (isset($_GET['lat'])?$_GET['lat']:'').'/';
|
||||
if (isset($_GET['lon'])) {
|
||||
$sOutQuery .= $_GET['lon'];
|
||||
}
|
||||
if (isset($_GET['zoom'])) {
|
||||
$sOutQuery .= '/'.$_GET['zoom'];
|
||||
}
|
||||
} else {
|
||||
$sOutQuery = $sQuery;
|
||||
}
|
||||
|
||||
$hLog = array(
|
||||
date('Y-m-d H:i:s', $aStartTime[0]).'.'.$aStartTime[1],
|
||||
$_SERVER['REMOTE_ADDR'],
|
||||
$_SERVER['QUERY_STRING'],
|
||||
$sOutQuery,
|
||||
$sType,
|
||||
$fStartTime
|
||||
);
|
||||
|
||||
if (CONST_Log_DB) {
|
||||
if (isset($_GET['email'])) {
|
||||
$sUserAgent = $_GET['email'];
|
||||
} elseif (isset($_SERVER['HTTP_REFERER'])) {
|
||||
$sUserAgent = $_SERVER['HTTP_REFERER'];
|
||||
} elseif (isset($_SERVER['HTTP_USER_AGENT'])) {
|
||||
$sUserAgent = $_SERVER['HTTP_USER_AGENT'];
|
||||
} else {
|
||||
$sUserAgent = '';
|
||||
}
|
||||
$sSQL = 'insert into new_query_log (type,starttime,query,ipaddress,useragent,language,format,searchterm)';
|
||||
$sSQL .= ' values (';
|
||||
$sSQL .= join(',', $oDB->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);
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
function formatOSMType($sType, $bIncludeExternal = true)
|
||||
{
|
||||
if ($sType == 'N') {
|
||||
return 'node';
|
||||
}
|
||||
if ($sType == 'W') {
|
||||
return 'way';
|
||||
}
|
||||
if ($sType == 'R') {
|
||||
return 'relation';
|
||||
}
|
||||
|
||||
if (!$bIncludeExternal) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($sType == 'T') {
|
||||
return 'way';
|
||||
}
|
||||
if ($sType == 'I') {
|
||||
return 'way';
|
||||
}
|
||||
|
||||
// not handled: P, L
|
||||
|
||||
return '';
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
function getOsm2pgsqlBinary()
|
||||
{
|
||||
$sBinary = getSetting('OSM2PGSQL_BINARY');
|
||||
|
||||
return $sBinary ? $sBinary : CONST_Default_Osm2pgsql;
|
||||
}
|
||||
|
||||
function getImportStyle()
|
||||
{
|
||||
$sStyle = getSetting('IMPORT_STYLE');
|
||||
|
||||
if (in_array($sStyle, array('admin', 'street', 'address', 'full', 'extratags'))) {
|
||||
return CONST_ConfigDir.'/import-'.$sStyle.'.style';
|
||||
}
|
||||
|
||||
return $sStyle;
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// https://github.com/geocoders/geocodejson-spec/
|
||||
|
||||
$aFilteredPlaces = array();
|
||||
|
||||
if (empty($aPlace)) {
|
||||
if (isset($sError)) {
|
||||
$aFilteredPlaces['error'] = $sError;
|
||||
} else {
|
||||
$aFilteredPlaces['error'] = 'Unable to geocode';
|
||||
}
|
||||
javascript_renderData($aFilteredPlaces);
|
||||
} else {
|
||||
$aFilteredPlaces = array(
|
||||
'type' => '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)
|
||||
));
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
$aFilteredPlaces = array();
|
||||
|
||||
if (empty($aPlace)) {
|
||||
if (isset($sError)) {
|
||||
$aFilteredPlaces['error'] = $sError;
|
||||
} else {
|
||||
$aFilteredPlaces['error'] = 'Unable to geocode';
|
||||
}
|
||||
javascript_renderData($aFilteredPlaces);
|
||||
} else {
|
||||
$aFilteredPlaces = array(
|
||||
'type' => '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)
|
||||
));
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
$aFilteredPlaces = array();
|
||||
|
||||
if (empty($aPlace)) {
|
||||
if (isset($sError)) {
|
||||
$aFilteredPlaces['error'] = $sError;
|
||||
} else {
|
||||
$aFilteredPlaces['error'] = 'Unable to geocode';
|
||||
}
|
||||
} else {
|
||||
if (isset($aPlace['place_id'])) {
|
||||
$aFilteredPlaces['place_id'] = $aPlace['place_id'];
|
||||
}
|
||||
$aFilteredPlaces['licence'] = 'Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright';
|
||||
$sOSMType = formatOSMType($aPlace['osm_type']);
|
||||
if ($sOSMType) {
|
||||
$aFilteredPlaces['osm_type'] = $sOSMType;
|
||||
$aFilteredPlaces['osm_id'] = $aPlace['osm_id'];
|
||||
}
|
||||
if (isset($aPlace['lat'])) {
|
||||
$aFilteredPlaces['lat'] = $aPlace['lat'];
|
||||
}
|
||||
if (isset($aPlace['lon'])) {
|
||||
$aFilteredPlaces['lon'] = $aPlace['lon'];
|
||||
}
|
||||
|
||||
if ($sOutputFormat == 'jsonv2' || $sOutputFormat == 'geojson') {
|
||||
$aFilteredPlaces['place_rank'] = $aPlace['rank_search'];
|
||||
|
||||
$aFilteredPlaces['category'] = $aPlace['class'];
|
||||
$aFilteredPlaces['type'] = $aPlace['type'];
|
||||
|
||||
$aFilteredPlaces['importance'] = $aPlace['importance'];
|
||||
|
||||
$aFilteredPlaces['addresstype'] = strtolower($aPlace['addresstype']);
|
||||
|
||||
$aFilteredPlaces['name'] = $aPlace['placename'];
|
||||
}
|
||||
|
||||
$aFilteredPlaces['display_name'] = $aPlace['langaddress'];
|
||||
|
||||
if (isset($aPlace['address'])) {
|
||||
$aFilteredPlaces['address'] = $aPlace['address']->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);
|
@ -1,110 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
header('content-type: text/xml; charset=UTF-8');
|
||||
|
||||
echo '<';
|
||||
echo '?xml version="1.0" encoding="UTF-8" ?';
|
||||
echo ">\n";
|
||||
|
||||
echo '<reversegeocode';
|
||||
echo " timestamp='".date(DATE_RFC822)."'";
|
||||
echo " attribution='Data © OpenStreetMap contributors, ODbL 1.0. http://www.openstreetmap.org/copyright'";
|
||||
echo " querystring='".htmlspecialchars($_SERVER['QUERY_STRING'], ENT_QUOTES)."'";
|
||||
echo ">\n";
|
||||
|
||||
if (empty($aPlace)) {
|
||||
if (isset($sError)) {
|
||||
echo "<error>$sError</error>";
|
||||
} else {
|
||||
echo '<error>Unable to geocode</error>';
|
||||
}
|
||||
} else {
|
||||
echo '<result';
|
||||
if ($aPlace['place_id']) {
|
||||
echo ' place_id="'.$aPlace['place_id'].'"';
|
||||
}
|
||||
$sOSMType = formatOSMType($aPlace['osm_type']);
|
||||
if ($sOSMType) {
|
||||
echo ' osm_type="'.$sOSMType.'"'.' osm_id="'.$aPlace['osm_id'].'"';
|
||||
}
|
||||
if ($aPlace['ref']) {
|
||||
echo ' ref="'.htmlspecialchars($aPlace['ref']).'"';
|
||||
}
|
||||
if (isset($aPlace['lat'])) {
|
||||
echo ' lat="'.htmlspecialchars($aPlace['lat']).'"';
|
||||
}
|
||||
if (isset($aPlace['lon'])) {
|
||||
echo ' lon="'.htmlspecialchars($aPlace['lon']).'"';
|
||||
}
|
||||
if (isset($aPlace['aBoundingBox'])) {
|
||||
echo ' boundingbox="';
|
||||
echo join(',', $aPlace['aBoundingBox']);
|
||||
echo '"';
|
||||
}
|
||||
echo " place_rank='".$aPlace['rank_search']."'";
|
||||
echo " address_rank='".$aPlace['rank_address']."'";
|
||||
|
||||
|
||||
if (isset($aPlace['asgeojson'])) {
|
||||
echo ' geojson=\'';
|
||||
echo $aPlace['asgeojson'];
|
||||
echo '\'';
|
||||
}
|
||||
|
||||
if (isset($aPlace['assvg'])) {
|
||||
echo ' geosvg=\'';
|
||||
echo $aPlace['assvg'];
|
||||
echo '\'';
|
||||
}
|
||||
|
||||
if (isset($aPlace['astext'])) {
|
||||
echo ' geotext=\'';
|
||||
echo $aPlace['astext'];
|
||||
echo '\'';
|
||||
}
|
||||
echo '>'.htmlspecialchars($aPlace['langaddress']).'</result>';
|
||||
|
||||
if (isset($aPlace['address'])) {
|
||||
echo '<addressparts>';
|
||||
foreach ($aPlace['address']->getAddressNames() as $sKey => $sValue) {
|
||||
$sKey = str_replace(' ', '_', $sKey);
|
||||
echo "<$sKey>";
|
||||
echo htmlspecialchars($sValue);
|
||||
echo "</$sKey>";
|
||||
}
|
||||
echo '</addressparts>';
|
||||
}
|
||||
|
||||
if (isset($aPlace['sExtraTags'])) {
|
||||
echo '<extratags>';
|
||||
foreach ($aPlace['sExtraTags'] as $sKey => $sValue) {
|
||||
echo '<tag key="'.htmlspecialchars($sKey).'" value="'.htmlspecialchars($sValue).'"/>';
|
||||
}
|
||||
echo '</extratags>';
|
||||
}
|
||||
|
||||
if (isset($aPlace['sNameDetails'])) {
|
||||
echo '<namedetails>';
|
||||
foreach ($aPlace['sNameDetails'] as $sKey => $sValue) {
|
||||
echo '<name desc="'.htmlspecialchars($sKey).'">';
|
||||
echo htmlspecialchars($sValue);
|
||||
echo '</name>';
|
||||
}
|
||||
echo '</namedetails>';
|
||||
}
|
||||
|
||||
if (isset($aPlace['askml'])) {
|
||||
echo "\n<geokml>";
|
||||
echo $aPlace['askml'];
|
||||
echo '</geokml>';
|
||||
}
|
||||
}
|
||||
|
||||
echo '</reversegeocode>';
|
@ -1,120 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
$aPlaceDetails = array();
|
||||
|
||||
$aPlaceDetails['place_id'] = (int) $aPointDetails['place_id'];
|
||||
$aPlaceDetails['parent_place_id'] = (int) $aPointDetails['parent_place_id'];
|
||||
|
||||
$aPlaceDetails['osm_type'] = $aPointDetails['osm_type'];
|
||||
$aPlaceDetails['osm_id'] = (int) $aPointDetails['osm_id'];
|
||||
|
||||
$aPlaceDetails['category'] = $aPointDetails['class'];
|
||||
$aPlaceDetails['type'] = $aPointDetails['type'];
|
||||
$aPlaceDetails['admin_level'] = $aPointDetails['admin_level'];
|
||||
|
||||
$aPlaceDetails['localname'] = $aPointDetails['localname'];
|
||||
$aPlaceDetails['names'] = $aPointDetails['aNames'];
|
||||
|
||||
$aPlaceDetails['addresstags'] = $aPointDetails['aAddressTags'];
|
||||
$aPlaceDetails['housenumber'] = $aPointDetails['housenumber'];
|
||||
$aPlaceDetails['calculated_postcode'] = $aPointDetails['postcode'];
|
||||
$aPlaceDetails['country_code'] = $aPointDetails['country_code'];
|
||||
|
||||
$aPlaceDetails['indexed_date'] = (new DateTime('@'.$aPointDetails['indexed_epoch']))->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);
|
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
$error = array(
|
||||
'code' => $exception->getCode(),
|
||||
'message' => $exception->getMessage()
|
||||
);
|
||||
|
||||
if (CONST_Debug) {
|
||||
$error['details'] = $exception->getFile() . '('. $exception->getLine() . ')';
|
||||
}
|
||||
|
||||
javascript_renderData(array('error' => $error));
|
@ -1,7 +0,0 @@
|
||||
<error>
|
||||
<code><?php echo $exception->getCode() ?></code>
|
||||
<message><?php echo $exception->getMessage() ?></message>
|
||||
<?php if (CONST_Debug) { ?>
|
||||
<details><?php echo $exception->getFile() . '('. $exception->getLine() . ')' ?></details>
|
||||
<?php } ?>
|
||||
</error>
|
@ -1,83 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
$aOutput = array();
|
||||
$aOutput['licence'] = 'Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright';
|
||||
$aOutput['batch'] = array();
|
||||
|
||||
foreach ($aBatchResults as $aSearchResults) {
|
||||
if (!$aSearchResults) {
|
||||
$aSearchResults = array();
|
||||
}
|
||||
$aFilteredPlaces = array();
|
||||
foreach ($aSearchResults as $iResNum => $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'));
|
@ -1,72 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
$aFilteredPlaces = array();
|
||||
foreach ($aSearchResults as $iResNum => $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
|
||||
));
|
@ -1,83 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
$aFilteredPlaces = array();
|
||||
foreach ($aSearchResults as $iResNum => $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
|
||||
));
|
@ -1,81 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
$aFilteredPlaces = array();
|
||||
foreach ($aSearchResults as $iResNum => $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);
|
@ -1,138 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
header('content-type: text/xml; charset=UTF-8');
|
||||
|
||||
echo '<';
|
||||
echo '?xml version="1.0" encoding="UTF-8" ?';
|
||||
echo ">\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 "<place place_id='".$aResult['place_id']."'";
|
||||
$sOSMType = formatOSMType($aResult['osm_type']);
|
||||
if ($sOSMType) {
|
||||
echo " osm_type='$sOSMType'";
|
||||
echo " osm_id='".$aResult['osm_id']."'";
|
||||
}
|
||||
echo " place_rank='".$aResult['rank_search']."'";
|
||||
echo " address_rank='".$aResult['rank_address']."'";
|
||||
|
||||
if (isset($aResult['aBoundingBox'])) {
|
||||
echo ' boundingbox="';
|
||||
echo join(',', $aResult['aBoundingBox']);
|
||||
echo '"';
|
||||
}
|
||||
|
||||
if (isset($aResult['asgeojson'])) {
|
||||
echo ' geojson=\'';
|
||||
echo $aResult['asgeojson'];
|
||||
echo '\'';
|
||||
}
|
||||
|
||||
if (isset($aResult['assvg'])) {
|
||||
echo ' geosvg=\'';
|
||||
echo $aResult['assvg'];
|
||||
echo '\'';
|
||||
}
|
||||
|
||||
if (isset($aResult['astext'])) {
|
||||
echo ' geotext=\'';
|
||||
echo $aResult['astext'];
|
||||
echo '\'';
|
||||
}
|
||||
|
||||
if (isset($aResult['zoom'])) {
|
||||
echo " zoom='".$aResult['zoom']."'";
|
||||
}
|
||||
|
||||
echo " lat='".$aResult['lat']."'";
|
||||
echo " lon='".$aResult['lon']."'";
|
||||
echo " display_name='".htmlspecialchars($aResult['name'], ENT_QUOTES)."'";
|
||||
|
||||
echo " class='".htmlspecialchars($aResult['class'])."'";
|
||||
echo " type='".htmlspecialchars($aResult['type'], ENT_QUOTES)."'";
|
||||
echo " importance='".htmlspecialchars($aResult['importance'])."'";
|
||||
if (isset($aResult['icon']) && $aResult['icon']) {
|
||||
echo " icon='".htmlspecialchars($aResult['icon'], ENT_QUOTES)."'";
|
||||
}
|
||||
|
||||
$bHasDelim = false;
|
||||
|
||||
if (isset($aResult['askml'])) {
|
||||
if (!$bHasDelim) {
|
||||
$bHasDelim = true;
|
||||
echo '>';
|
||||
}
|
||||
echo "\n<geokml>";
|
||||
echo $aResult['askml'];
|
||||
echo '</geokml>';
|
||||
}
|
||||
|
||||
if (isset($aResult['sExtraTags'])) {
|
||||
if (!$bHasDelim) {
|
||||
$bHasDelim = true;
|
||||
echo '>';
|
||||
}
|
||||
echo "\n<extratags>";
|
||||
foreach ($aResult['sExtraTags'] as $sKey => $sValue) {
|
||||
echo '<tag key="'.htmlspecialchars($sKey).'" value="'.htmlspecialchars($sValue).'"/>';
|
||||
}
|
||||
echo '</extratags>';
|
||||
}
|
||||
|
||||
if (isset($aResult['sNameDetails'])) {
|
||||
if (!$bHasDelim) {
|
||||
$bHasDelim = true;
|
||||
echo '>';
|
||||
}
|
||||
echo "\n<namedetails>";
|
||||
foreach ($aResult['sNameDetails'] as $sKey => $sValue) {
|
||||
echo '<name desc="'.htmlspecialchars($sKey).'">';
|
||||
echo htmlspecialchars($sValue);
|
||||
echo '</name>';
|
||||
}
|
||||
echo '</namedetails>';
|
||||
}
|
||||
|
||||
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 "</$sKey>";
|
||||
}
|
||||
}
|
||||
|
||||
if ($bHasDelim) {
|
||||
echo '</place>';
|
||||
} else {
|
||||
echo '/>';
|
||||
}
|
||||
}
|
||||
|
||||
echo '</' . (isset($sXmlRootTag)?$sXmlRootTag:'searchresults') . '>';
|
@ -1,235 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
require_once(CONST_LibDir.'/SimpleWordList.php');
|
||||
|
||||
class Tokenizer
|
||||
{
|
||||
private $oDB;
|
||||
|
||||
private $oNormalizer;
|
||||
private $oTransliterator;
|
||||
|
||||
public function __construct(&$oDB)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,265 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Nominatim;
|
||||
|
||||
require_once(CONST_LibDir.'/SimpleWordList.php');
|
||||
|
||||
class Tokenizer
|
||||
{
|
||||
private $oDB;
|
||||
|
||||
private $oNormalizer = null;
|
||||
|
||||
public function __construct(&$oDB)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
require_once(CONST_LibDir.'/init-website.php');
|
||||
require_once(CONST_LibDir.'/log.php');
|
||||
require_once(CONST_LibDir.'/output.php');
|
||||
ini_set('memory_limit', '200M');
|
||||
|
||||
$oParams = new Nominatim\ParameterParser();
|
||||
$sOutputFormat = $oParams->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);
|
||||
}
|
@ -1,263 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
require_once(CONST_LibDir.'/init-website.php');
|
||||
require_once(CONST_LibDir.'/log.php');
|
||||
require_once(CONST_LibDir.'/output.php');
|
||||
require_once(CONST_LibDir.'/AddressDetails.php');
|
||||
ini_set('memory_limit', '200M');
|
||||
|
||||
$oParams = new Nominatim\ParameterParser();
|
||||
|
||||
$sOutputFormat = $oParams->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');
|
@ -1,101 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
require_once(CONST_LibDir.'/init-website.php');
|
||||
require_once(CONST_LibDir.'/log.php');
|
||||
require_once(CONST_LibDir.'/PlaceLookup.php');
|
||||
require_once(CONST_LibDir.'/output.php');
|
||||
ini_set('memory_limit', '200M');
|
||||
|
||||
$oParams = new Nominatim\ParameterParser();
|
||||
|
||||
// Format for output
|
||||
$sOutputFormat = $oParams->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');
|
@ -1,63 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
require_once(CONST_LibDir.'/init-website.php');
|
||||
require_once(CONST_LibDir.'/log.php');
|
||||
require_once(CONST_LibDir.'/output.php');
|
||||
ini_set('memory_limit', '200M');
|
||||
|
||||
$oParams = new Nominatim\ParameterParser();
|
||||
$sOutputFormat = $oParams->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);
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
require_once(CONST_LibDir.'/init-website.php');
|
||||
require_once(CONST_LibDir.'/ParameterParser.php');
|
||||
|
||||
$oParams = new Nominatim\ParameterParser();
|
||||
|
||||
// Format for output
|
||||
$sOutputFormat = $oParams->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);
|
@ -1,95 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
require_once(CONST_LibDir.'/init-website.php');
|
||||
require_once(CONST_LibDir.'/log.php');
|
||||
require_once(CONST_LibDir.'/PlaceLookup.php');
|
||||
require_once(CONST_LibDir.'/ReverseGeocode.php');
|
||||
require_once(CONST_LibDir.'/output.php');
|
||||
ini_set('memory_limit', '200M');
|
||||
|
||||
$oParams = new Nominatim\ParameterParser();
|
||||
|
||||
// Format for output
|
||||
$sOutputFormat = $oParams->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');
|
@ -1,93 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
require_once(CONST_LibDir.'/init-website.php');
|
||||
require_once(CONST_LibDir.'/log.php');
|
||||
require_once(CONST_LibDir.'/Geocode.php');
|
||||
require_once(CONST_LibDir.'/output.php');
|
||||
ini_set('memory_limit', '200M');
|
||||
|
||||
$oDB = new Nominatim\DB(CONST_Database_DSN);
|
||||
$oDB->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);
|
||||
|
||||
$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');
|
@ -1,56 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
require_once(CONST_LibDir.'/init-website.php');
|
||||
require_once(CONST_LibDir.'/ParameterParser.php');
|
||||
require_once(CONST_LibDir.'/Status.php');
|
||||
|
||||
$oParams = new Nominatim\ParameterParser();
|
||||
$sOutputFormat = $oParams->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',
|
||||
'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()
|
||||
);
|
||||
javascript_renderData($aResponse);
|
||||
} else {
|
||||
header('HTTP/1.0 500 Internal Server Error');
|
||||
echo 'ERROR: '.$oErr->getMessage();
|
||||
}
|
||||
}
|
@ -26,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'
|
||||
|
@ -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()
|
||||
|
@ -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')
|
||||
|
@ -177,16 +177,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
|
||||
|
||||
|
@ -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
|
||||
@ -154,10 +153,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 +166,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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -74,7 +74,6 @@ class Configuration:
|
||||
class _LibDirs:
|
||||
module: Path
|
||||
osm2pgsql: Path
|
||||
php = paths.PHPLIB_DIR
|
||||
sql = paths.SQLLIB_DIR
|
||||
data = paths.DATA_DIR
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
@ -281,22 +277,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"""\
|
||||
<?php
|
||||
@define('CONST_Max_Word_Frequency', 10000000);
|
||||
@define('CONST_Term_Normalization_Rules', "{self.loader.normalization_rules}");
|
||||
@define('CONST_Transliteration', "{self.loader.get_search_rules()}");
|
||||
require_once('{phpdir}/tokenizer/icu_tokenizer.php');"""), encoding='utf-8')
|
||||
|
||||
|
||||
def _save_config(self) -> None:
|
||||
""" Save the configuration that needs to remain stable for the given
|
||||
database as database properties.
|
||||
|
@ -14,7 +14,6 @@ import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
from textwrap import dedent
|
||||
|
||||
from icu import Transliterator
|
||||
import psycopg
|
||||
@ -120,8 +119,6 @@ class LegacyTokenizer(AbstractTokenizer):
|
||||
|
||||
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)
|
||||
@ -145,8 +142,6 @@ class LegacyTokenizer(AbstractTokenizer):
|
||||
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.
|
||||
@ -272,21 +267,6 @@ class LegacyTokenizer(AbstractTokenizer):
|
||||
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"""\
|
||||
<?php
|
||||
@define('CONST_Max_Word_Frequency', {config.MAX_WORD_FREQUENCY});
|
||||
@define('CONST_Term_Normalization_Rules', "{config.TERM_NORMALIZATION}");
|
||||
require_once('{config.lib_dir.php}/tokenizer/legacy_tokenizer.php');
|
||||
"""), encoding='utf-8')
|
||||
|
||||
|
||||
def _init_db_tables(self, config: Configuration) -> None:
|
||||
""" Set up the word table and fill it with pre-computed word
|
||||
frequencies.
|
||||
|
@ -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.
|
||||
|
@ -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"""\
|
||||
<?php
|
||||
|
||||
@define('CONST_Debug', $_GET['debug'] ?? false);
|
||||
@define('CONST_LibDir', '{config.lib_dir.php}');
|
||||
@define('CONST_TokenizerDir', '{config.project_dir / 'tokenizer'}');
|
||||
@define('CONST_NominatimVersion', '{NOMINATIM_VERSION!s}');
|
||||
|
||||
""")
|
||||
|
||||
for php_name, conf_name, var_type in PHP_CONST_DEFS:
|
||||
varout = _quote_php_variable(var_type, config, conf_name)
|
||||
|
||||
basedata += f"@define('CONST_{php_name}', {varout});\n"
|
||||
|
||||
template = "\nrequire_once(CONST_LibDir.'/website/{}');\n"
|
||||
|
||||
search_name_table_exists = bool(conn and table_exists(conn, 'search_name'))
|
||||
|
||||
for script in WEBSITE_SCRIPTS:
|
||||
if not search_name_table_exists and script == 'search.php':
|
||||
out = template.format('reverse-only-search.php')
|
||||
else:
|
||||
out = template.format(script)
|
||||
|
||||
(basedir / script).write_text(basedata + out, 'utf-8')
|
||||
|
||||
|
||||
def invalidate_osm_object(osm_type: str, osm_id: int, conn: Connection,
|
||||
recursive: bool = True) -> None:
|
||||
""" Mark the given OSM object for reindexing. When 'recursive' is set
|
||||
|
@ -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
|
||||
|
@ -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 <feature>
|
||||
Then the result is valid html
|
||||
|
@ -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 |
|
||||
|
@ -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 |
|
||||
|
@ -353,19 +353,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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -98,7 +98,6 @@ Feature: Querying fo postcode variants
|
||||
|
||||
|
||||
@fail-legacy
|
||||
@v1-api-python-only
|
||||
Scenario: Postcode areas are preferred over postcode points
|
||||
Given the grid with origin DE
|
||||
| 1 | 2 |
|
||||
|
@ -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 |
|
||||
|
@ -66,9 +66,3 @@ 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.")
|
||||
|
@ -44,11 +44,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 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.")
|
||||
@ -110,15 +108,6 @@ class NominatimEnvironment:
|
||||
|
||||
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)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user