2017-10-06 00:03:03 +03:00
< ? php
2017-10-08 18:00:59 +03:00
namespace Nominatim ;
2017-10-06 00:03:03 +03:00
2020-12-15 12:09:55 +03:00
require_once ( CONST_LibDir . '/SpecialSearchOperator.php' );
require_once ( CONST_LibDir . '/SearchContext.php' );
require_once ( CONST_LibDir . '/Result.php' );
2017-10-08 18:13:41 +03:00
2017-10-06 00:03:03 +03:00
/**
* 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 ();
2018-05-06 23:10:38 +03:00
/// True if the name is rare enough to force index use on name.
private $bRareName = false ;
2017-10-06 00:03:03 +03:00
/// 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 = '' ;
2017-10-08 22:23:31 +03:00
/// Global search constraints.
private $oContext ;
2017-10-06 00:03:03 +03:00
// Temporary values used while creating the search description.
2017-10-08 22:23:31 +03:00
/// Index of phrase currently processed.
2017-10-06 00:03:03 +03:00
private $iNamePhrase = - 1 ;
2017-10-06 01:14:48 +03:00
2017-10-10 01:15:56 +03:00
/**
* Create an empty search description .
*
* @ param object $oContext Global context to use . Will be inherited by
* all derived search objects .
*/
2017-10-08 22:23:31 +03:00
public function __construct ( $oContext )
{
$this -> oContext = $oContext ;
}
2017-10-10 01:15:56 +03:00
/**
* Get current search rank .
*
2018-02-18 16:11:35 +03:00
* The higher the search rank the lower the likelihood that the
2017-10-10 01:15:56 +03:00
* search is a correct interpretation of the search query .
*
* @ return integer Search rank .
*/
2017-10-07 13:01:56 +03:00
public function getRank ()
2017-10-06 01:14:48 +03:00
{
return $this -> iSearchRank ;
}
2017-10-10 01:15:56 +03:00
/**
* 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
*/
2017-10-07 13:01:56 +03:00
public function setPoiSearch ( $iOperator , $sClass , $sType )
2017-10-06 01:14:48 +03:00
{
$this -> iOperator = $iOperator ;
$this -> sClass = $sClass ;
$this -> sType = $sType ;
}
2017-10-10 01:15:56 +03:00
/**
* Check if any operator is set .
*
* @ return bool True , if this is a special search operation .
*/
2017-10-07 13:01:56 +03:00
public function hasOperator ()
2017-10-06 01:14:48 +03:00
{
return $this -> iOperator != Operator :: NONE ;
}
2017-10-10 01:15:56 +03:00
/**
* 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 .
*/
2017-10-07 13:01:56 +03:00
public function extractKeyValuePairs ( $sQuery )
2017-10-06 01:14:48 +03:00
{
// 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 ;
}
2017-10-07 13:01:56 +03:00
2017-10-10 01:15:56 +03:00
/**
* Check if the combination of parameters is sensible .
*
* @ return bool True , if the search looks valid .
*/
2017-10-13 23:23:39 +03:00
public function isValidSearch ()
2017-10-08 13:57:22 +03:00
{
2018-03-22 14:36:24 +03:00
if ( empty ( $this -> aName )) {
2017-10-08 13:57:22 +03:00
if ( $this -> sHouseNumber ) {
return false ;
}
2017-10-13 23:23:39 +03:00
if ( ! $this -> sClass && ! $this -> sCountryCode ) {
return false ;
}
2017-10-08 13:57:22 +03:00
}
return true ;
}
/////////// Search building functions
2017-10-08 18:00:59 +03:00
2017-10-10 01:15:56 +03:00
/**
* Derive new searches by adding a full term to the existing search .
*
2021-07-14 23:17:17 +03:00
* @ param string $sToken Term for the token .
* @ param object $oSearchTerm Description of the token .
* @ param string $sPhraseType Type of phrase the token is contained in .
* @ param bool $bFirstToken True if the token is at the beginning of the
* query .
* @ param bool $bLastToken True if the token is at the end of the query .
* @ param integer $iPhrase Number of the phrase the token is in .
2017-10-10 01:15:56 +03:00
*
* @ return SearchDescription [] List of derived search descriptions .
*/
2021-07-14 23:17:17 +03:00
public function extendWithSearchTerm ( $sToken , $oSearchTerm , $sPhraseType , $bFirstToken , $bLastToken , $iPhrase )
2017-10-08 13:57:22 +03:00
{
$aNewSearches = array ();
if (( $sPhraseType == '' || $sPhraseType == 'country' )
2018-05-15 00:04:15 +03:00
&& is_a ( $oSearchTerm , '\Nominatim\Token\Country' )
2017-10-08 13:57:22 +03:00
) {
if ( ! $this -> sCountryCode ) {
$oSearch = clone $this ;
$oSearch -> iSearchRank ++ ;
2018-05-15 00:04:15 +03:00
$oSearch -> sCountryCode = $oSearchTerm -> sCountryCode ;
2017-10-08 13:57:22 +03:00
// Country is almost always at the end of the string
// - increase score for finding it anywhere else (optimisation)
if ( ! $bLastToken ) {
$oSearch -> iSearchRank += 5 ;
2021-03-12 00:44:49 +03:00
$oSearch -> iNamePhrase = - 1 ;
2017-10-08 13:57:22 +03:00
}
$aNewSearches [] = $oSearch ;
}
} elseif (( $sPhraseType == '' || $sPhraseType == 'postalcode' )
2018-05-15 00:04:15 +03:00
&& is_a ( $oSearchTerm , '\Nominatim\Token\Postcode' )
2017-10-08 13:57:22 +03:00
) {
2018-05-15 00:04:15 +03:00
if ( ! $this -> sPostcode ) {
2017-10-08 13:57:22 +03:00
// If we have structured search or this is the first term,
// make the postcode the primary search element.
2020-10-06 15:00:43 +03:00
if ( $this -> iOperator == Operator :: NONE && $bFirstToken ) {
2017-10-08 13:57:22 +03:00
$oSearch = clone $this ;
$oSearch -> iSearchRank ++ ;
$oSearch -> iOperator = Operator :: POSTCODE ;
$oSearch -> aAddress = array_merge ( $this -> aAddress , $this -> aName );
$oSearch -> aName =
2018-05-15 00:04:15 +03:00
array ( $oSearchTerm -> iId => $oSearchTerm -> sPostcode );
2017-10-08 13:57:22 +03:00
$aNewSearches [] = $oSearch ;
}
// If we have a structured search or this is not the first term,
// add the postcode as an addendum.
if ( $this -> iOperator != Operator :: POSTCODE
2018-03-22 14:36:24 +03:00
&& ( $sPhraseType == 'postalcode' || ! empty ( $this -> aName ))
2017-10-08 13:57:22 +03:00
) {
$oSearch = clone $this ;
$oSearch -> iSearchRank ++ ;
2021-03-12 00:44:49 +03:00
$oSearch -> iNamePhrase = - 1 ;
2020-11-25 13:44:25 +03:00
if ( strlen ( $oSearchTerm -> sPostcode ) < 4 ) {
$oSearch -> iSearchRank += 4 - strlen ( $oSearchTerm -> sPostcode );
}
2018-05-15 00:04:15 +03:00
$oSearch -> sPostcode = $oSearchTerm -> sPostcode ;
2017-10-08 13:57:22 +03:00
$aNewSearches [] = $oSearch ;
}
}
} elseif (( $sPhraseType == '' || $sPhraseType == 'street' )
2018-05-15 00:04:15 +03:00
&& is_a ( $oSearchTerm , '\Nominatim\Token\HouseNumber' )
2017-10-08 13:57:22 +03:00
) {
if ( ! $this -> sHouseNumber && $this -> iOperator != Operator :: POSTCODE ) {
// sanity check: if the housenumber is not mainly made
// up of numbers, add a penalty
2021-06-26 11:31:55 +03:00
$iSearchCost = 1 ;
if ( preg_match ( '/\\d/' , $oSearchTerm -> sToken ) === 0
|| preg_match_all ( '/[^0-9]/' , $oSearchTerm -> sToken , $aMatches ) > 2 ) {
$iSearchCost ++ ;
}
if ( $this -> iOperator != Operator :: NONE ) {
$iSearchCost ++ ;
2017-10-08 13:57:22 +03:00
}
2018-05-15 00:04:15 +03:00
if ( empty ( $oSearchTerm -> iId )) {
2021-06-26 11:31:55 +03:00
$iSearchCost ++ ;
2017-10-08 18:36:38 +03:00
}
2017-10-08 13:57:22 +03:00
// also must not appear in the middle of the address
2018-03-22 14:36:24 +03:00
if ( ! empty ( $this -> aAddress )
|| ( ! empty ( $this -> aAddressNonSearch ))
2017-10-25 00:30:41 +03:00
|| $this -> sPostcode
) {
2021-06-26 11:31:55 +03:00
$iSearchCost ++ ;
2017-10-08 13:57:22 +03:00
}
2021-06-26 11:31:55 +03:00
$oSearch = clone $this ;
$oSearch -> iSearchRank += $iSearchCost ;
$oSearch -> iNamePhrase = - 1 ;
$oSearch -> sHouseNumber = $oSearchTerm -> sToken ;
2017-10-08 13:57:22 +03:00
$aNewSearches [] = $oSearch ;
2021-06-26 12:20:25 +03:00
2020-11-19 14:06:53 +03:00
// Housenumbers may appear in the name when the place has its own
// address terms.
2020-11-25 18:11:32 +03:00
if ( $oSearchTerm -> iId !== null
&& ( $this -> iNamePhrase >= 0 || empty ( $this -> aName ))
&& empty ( $this -> aAddress )
) {
2020-11-19 14:06:53 +03:00
$oSearch = clone $this ;
2021-06-26 11:31:55 +03:00
$oSearch -> iSearchRank += $iSearchCost ;
2020-11-19 14:06:53 +03:00
$oSearch -> aAddress = $this -> aName ;
2020-12-08 19:25:15 +03:00
$oSearch -> bRareName = false ;
2020-11-19 14:06:53 +03:00
$oSearch -> aName = array ( $oSearchTerm -> iId => $oSearchTerm -> iId );
$aNewSearches [] = $oSearch ;
}
2017-10-08 13:57:22 +03:00
}
2018-05-15 00:04:15 +03:00
} elseif ( $sPhraseType == ''
&& is_a ( $oSearchTerm , '\Nominatim\Token\SpecialTerm' )
) {
2017-10-14 00:04:12 +03:00
if ( $this -> iOperator == Operator :: NONE ) {
2017-10-08 16:26:14 +03:00
$oSearch = clone $this ;
2021-03-11 22:34:21 +03:00
$oSearch -> iSearchRank += 2 ;
2021-03-12 00:44:49 +03:00
$oSearch -> iNamePhrase = - 1 ;
2017-10-08 13:57:22 +03:00
2018-05-15 00:04:15 +03:00
$iOp = $oSearchTerm -> iOperator ;
if ( $iOp == Operator :: NONE ) {
2018-03-22 14:36:24 +03:00
if ( ! empty ( $this -> aName ) || $this -> oContext -> isBoundedSearch ()) {
2017-10-08 13:57:22 +03:00
$iOp = Operator :: NAME ;
2018-05-15 00:04:15 +03:00
} else {
$iOp = Operator :: NEAR ;
2017-10-08 13:57:22 +03:00
}
$oSearch -> iSearchRank += 2 ;
2021-03-11 23:14:23 +03:00
} elseif ( ! $bFirstToken && ! $bLastToken ) {
2021-03-11 22:34:21 +03:00
$oSearch -> iSearchRank += 2 ;
}
if ( $this -> sHouseNumber ) {
$oSearch -> iSearchRank ++ ;
2017-10-08 13:57:22 +03:00
}
2018-05-15 00:04:15 +03:00
$oSearch -> setPoiSearch (
$iOp ,
$oSearchTerm -> sClass ,
$oSearchTerm -> sType
);
2017-10-08 17:42:04 +03:00
$aNewSearches [] = $oSearch ;
2017-10-08 13:57:22 +03:00
}
2018-05-15 00:04:15 +03:00
} elseif ( $sPhraseType != 'country'
&& is_a ( $oSearchTerm , '\Nominatim\Token\Word' )
2017-10-13 23:23:39 +03:00
) {
2018-05-15 00:04:15 +03:00
$iWordID = $oSearchTerm -> iId ;
2018-03-02 02:26:48 +03:00
// Full words can only be a name if they appear at the beginning
// of the phrase. In structured search the name must forcably in
// the first phrase. In unstructured search it may be in a later
// phrase when the first phrase is a house number.
2021-07-14 23:17:17 +03:00
if ( ! empty ( $this -> aName ) || ! ( $iPhrase == 0 || $sPhraseType == '' )) {
if (( $sPhraseType == '' || $iPhrase > 0 ) && $oSearchTerm -> iTermCount > 1 ) {
2017-10-08 13:57:22 +03:00
$oSearch = clone $this ;
2021-03-12 00:44:49 +03:00
$oSearch -> iNamePhrase = - 1 ;
2021-06-26 12:20:25 +03:00
$oSearch -> iSearchRank += 1 ;
2017-10-08 13:57:22 +03:00
$oSearch -> aAddress [ $iWordID ] = $iWordID ;
$aNewSearches [] = $oSearch ;
}
2021-03-11 23:14:23 +03:00
} elseif ( empty ( $this -> aNameNonSearch )) {
2018-03-02 02:26:48 +03:00
$oSearch = clone $this ;
$oSearch -> iSearchRank ++ ;
$oSearch -> aName = array ( $iWordID => $iWordID );
2018-05-06 23:10:38 +03:00
if ( CONST_Search_NameOnlySearchFrequencyThreshold ) {
$oSearch -> bRareName =
2018-05-15 00:04:15 +03:00
$oSearchTerm -> iSearchNameCount
2018-05-06 23:10:38 +03:00
< CONST_Search_NameOnlySearchFrequencyThreshold ;
}
2018-03-02 02:26:48 +03:00
$aNewSearches [] = $oSearch ;
2017-10-08 13:57:22 +03:00
}
2021-07-14 23:17:17 +03:00
} elseif ( $sPhraseType != 'country'
&& is_a ( $oSearchTerm , '\Nominatim\Token\Partial' )
&& strpos ( $sToken , ' ' ) === false
) {
$aNewSearches = $this -> extendWithPartialTerm (
$sToken ,
$oSearchTerm ,
( bool ) $sPhraseType ,
$iPhrase
);
2017-10-08 13:57:22 +03:00
}
return $aNewSearches ;
}
2017-10-10 01:15:56 +03:00
/**
* Derive new searches by adding a partial term to the existing search .
*
2018-05-15 00:04:15 +03:00
* @ param string $sToken Term for the token .
* @ param object $oSearchTerm Description of the token .
2017-10-14 00:11:09 +03:00
* @ param bool $bStructuredPhrases True if the search is structured .
* @ param integer $iPhrase Number of the phrase the token is in .
2017-10-10 01:15:56 +03:00
*
* @ return SearchDescription [] List of derived search descriptions .
*/
2021-07-14 23:17:17 +03:00
private function extendWithPartialTerm ( $sToken , $oSearchTerm , $bStructuredPhrases , $iPhrase )
2017-10-08 13:57:22 +03:00
{
$aNewSearches = array ();
2018-05-15 00:04:15 +03:00
$iWordID = $oSearchTerm -> iId ;
2017-10-08 13:57:22 +03:00
if (( ! $bStructuredPhrases || $iPhrase > 0 )
2018-03-22 14:36:24 +03:00
&& ( ! empty ( $this -> aName ))
2017-10-08 13:57:22 +03:00
) {
2021-03-11 17:03:36 +03:00
$oSearch = clone $this ;
$oSearch -> iSearchRank ++ ;
if ( preg_match ( '#^[0-9 ]+$#' , $sToken )) {
$oSearch -> iSearchRank ++ ;
}
2018-05-15 00:04:15 +03:00
if ( $oSearchTerm -> iSearchNameCount < CONST_Max_Word_Frequency ) {
2017-10-08 13:57:22 +03:00
$oSearch -> aAddress [ $iWordID ] = $iWordID ;
} else {
$oSearch -> aAddressNonSearch [ $iWordID ] = $iWordID ;
2017-10-08 18:00:59 +03:00
}
2021-03-11 17:03:36 +03:00
$aNewSearches [] = $oSearch ;
2017-10-08 13:57:22 +03:00
}
if (( ! $this -> sPostcode && ! $this -> aAddress && ! $this -> aAddressNonSearch )
2021-03-11 19:34:23 +03:00
&& (( empty ( $this -> aName ) && empty ( $this -> aNameNonSearch )) || $this -> iNamePhrase == $iPhrase )
2017-10-08 13:57:22 +03:00
) {
$oSearch = clone $this ;
2021-03-11 17:03:36 +03:00
$oSearch -> iSearchRank ++ ;
2021-03-11 19:34:23 +03:00
if ( empty ( $this -> aName ) && empty ( $this -> aNameNonSearch )) {
$oSearch -> iSearchRank ++ ;
}
2021-03-11 17:03:36 +03:00
if ( preg_match ( '#^[0-9 ]+$#' , $sToken )) {
$oSearch -> iSearchRank ++ ;
2017-10-08 13:57:22 +03:00
}
2018-05-15 00:04:15 +03:00
if ( $oSearchTerm -> iSearchNameCount < CONST_Max_Word_Frequency ) {
if ( empty ( $this -> aName )
&& CONST_Search_NameOnlySearchFrequencyThreshold
) {
2018-05-06 23:10:38 +03:00
$oSearch -> bRareName =
2018-05-15 00:04:15 +03:00
$oSearchTerm -> iSearchNameCount
2018-05-06 23:10:38 +03:00
< CONST_Search_NameOnlySearchFrequencyThreshold ;
} else {
$oSearch -> bRareName = false ;
}
2017-10-08 13:57:22 +03:00
$oSearch -> aName [ $iWordID ] = $iWordID ;
} else {
2017-10-08 16:26:14 +03:00
$oSearch -> aNameNonSearch [ $iWordID ] = $iWordID ;
2017-10-08 13:57:22 +03:00
}
$oSearch -> iNamePhrase = $iPhrase ;
2017-10-08 16:26:14 +03:00
$aNewSearches [] = $oSearch ;
2017-10-08 13:57:22 +03:00
}
return $aNewSearches ;
}
/////////// Query functions
2017-10-10 00:12:13 +03:00
2017-10-10 01:15:56 +03:00
/**
* Query database for places that match this search .
*
2019-03-10 17:42:58 +03:00
* @ param object $oDB Nominatim :: DB instance to use .
2018-05-06 23:10:38 +03:00
* @ 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 .
2017-10-10 01:15:56 +03:00
*
* @ return mixed [] An array with two fields : IDs contains the list of
* matching place IDs and houseNumber the houseNumber
* if appicable or - 1 if not .
*/
2018-05-06 23:10:38 +03:00
public function query ( & $oDB , $iMinRank , $iMaxRank , $iLimit )
2017-10-09 23:55:50 +03:00
{
2017-10-19 00:54:47 +03:00
$aResults = array ();
2017-10-09 23:55:50 +03:00
if ( $this -> sCountryCode
2018-03-22 14:36:24 +03:00
&& empty ( $this -> aName )
2017-10-09 23:55:50 +03:00
&& ! $this -> iOperator
&& ! $this -> sClass
&& ! $this -> oContext -> hasNearPoint ()
) {
// Just looking for a country - look it up
if ( 4 >= $iMinRank && 4 <= $iMaxRank ) {
2017-10-19 00:54:47 +03:00
$aResults = $this -> queryCountry ( $oDB );
2017-10-09 23:55:50 +03:00
}
2018-03-22 14:36:24 +03:00
} elseif ( empty ( $this -> aName ) && empty ( $this -> aAddress )) {
2017-10-09 23:55:50 +03:00
// Neither name nor address? Then we must be
// looking for a POI in a geographic area.
if ( $this -> oContext -> isBoundedSearch ()) {
2017-10-19 00:54:47 +03:00
$aResults = $this -> queryNearbyPoi ( $oDB , $iLimit );
2017-10-09 23:55:50 +03:00
}
} elseif ( $this -> iOperator == Operator :: POSTCODE ) {
// looking for postcode
2017-10-19 00:54:47 +03:00
$aResults = $this -> queryPostcode ( $oDB , $iLimit );
2017-10-09 23:55:50 +03:00
} else {
// Ordinary search:
// First search for places according to name and address.
2017-10-19 00:54:47 +03:00
$aResults = $this -> queryNamedPlace (
2017-10-09 23:55:50 +03:00
$oDB ,
$iMinRank ,
$iMaxRank ,
$iLimit
);
2019-10-30 20:49:02 +03:00
// Now search for housenumber, if housenumber provided. Can be zero.
if (( $this -> sHouseNumber || $this -> sHouseNumber === '0' ) && ! empty ( $aResults )) {
2021-06-17 13:05:33 +03:00
$aHnResults = $this -> queryHouseNumber ( $oDB , $aResults );
2018-11-17 02:35:38 +03:00
// Downgrade the rank of the street results, they are missing
2021-06-17 13:05:33 +03:00
// the housenumber. Also drop POI places (rank 30) here, they
// cannot be a parent place and therefore must not be shown
// as a result for a search with a missing housenumber.
2018-11-17 02:35:38 +03:00
foreach ( $aResults as $oRes ) {
2021-06-17 13:05:33 +03:00
if ( $oRes -> iAddressRank < 28 ) {
if ( $oRes -> iAddressRank >= 26 ) {
$oRes -> iResultRank ++ ;
} else {
$oRes -> iResultRank += 2 ;
}
$aHnResults [ $oRes -> iId ] = $oRes ;
2021-02-16 19:47:06 +03:00
}
2018-11-17 02:35:38 +03:00
}
2021-06-17 13:05:33 +03:00
$aResults = $aHnResults ;
2017-10-09 23:55:50 +03:00
}
// finally get POIs if requested
2018-03-22 14:36:24 +03:00
if ( $this -> sClass && ! empty ( $aResults )) {
2017-10-19 00:54:47 +03:00
$aResults = $this -> queryPoiByOperator ( $oDB , $aResults , $iLimit );
2017-10-09 23:55:50 +03:00
}
}
2018-03-24 19:44:13 +03:00
Debug :: printDebugTable ( 'Place IDs' , $aResults );
2017-10-09 23:55:50 +03:00
2018-03-22 14:36:24 +03:00
if ( ! empty ( $aResults ) && $this -> sPostcode ) {
2017-10-19 00:54:47 +03:00
$sPlaceIds = Result :: joinIdsByTable ( $aResults , Result :: TABLE_PLACEX );
if ( $sPlaceIds ) {
$sSQL = 'SELECT place_id FROM placex' ;
$sSQL .= ' WHERE place_id in (' . $sPlaceIds . ')' ;
2018-11-17 01:52:19 +03:00
$sSQL .= " AND postcode != ' " . $this -> sPostcode . " ' " ;
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2019-03-10 17:42:58 +03:00
$aFilteredPlaceIDs = $oDB -> getCol ( $sSQL );
2017-10-19 00:54:47 +03:00
if ( $aFilteredPlaceIDs ) {
foreach ( $aFilteredPlaceIDs as $iPlaceId ) {
2018-11-17 01:52:19 +03:00
$aResults [ $iPlaceId ] -> iResultRank ++ ;
2017-10-19 00:54:47 +03:00
}
2017-10-09 23:55:50 +03:00
}
}
}
2017-10-19 00:54:47 +03:00
return $aResults ;
2017-10-09 23:55:50 +03:00
}
2017-10-08 18:00:59 +03:00
2017-10-09 23:55:50 +03:00
private function queryCountry ( & $oDB )
2017-10-07 13:01:56 +03:00
{
$sSQL = 'SELECT place_id FROM placex ' ;
$sSQL .= " WHERE country_code=' " . $this -> sCountryCode . " ' " ;
$sSQL .= ' AND rank_search = 4' ;
2017-10-08 23:44:01 +03:00
if ( $this -> oContext -> bViewboxBounded ) {
$sSQL .= ' AND ST_Intersects(' . $this -> oContext -> sqlViewboxSmall . ', geometry)' ;
2017-10-07 13:01:56 +03:00
}
2017-10-26 22:21:21 +03:00
$sSQL .= ' ORDER BY st_area(geometry) DESC LIMIT 1' ;
2017-10-07 13:01:56 +03:00
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2017-10-07 13:01:56 +03:00
2019-02-24 18:14:36 +03:00
$iPlaceId = $oDB -> getOne ( $sSQL );
2017-10-19 00:54:47 +03:00
$aResults = array ();
2019-02-24 18:14:36 +03:00
if ( $iPlaceId ) {
2017-10-19 00:54:47 +03:00
$aResults [ $iPlaceId ] = new Result ( $iPlaceId );
}
return $aResults ;
2017-10-07 13:01:56 +03:00
}
2017-10-09 23:55:50 +03:00
private function queryNearbyPoi ( & $oDB , $iLimit )
2017-10-07 13:01:56 +03:00
{
if ( ! $this -> sClass ) {
return array ();
}
2017-10-19 00:54:47 +03:00
$aDBResults = array ();
2017-10-07 13:01:56 +03:00
$sPoiTable = $this -> poiTable ();
2019-03-10 17:42:58 +03:00
if ( $oDB -> tableExists ( $sPoiTable )) {
2017-10-07 13:01:56 +03:00
$sSQL = 'SELECT place_id FROM ' . $sPoiTable . ' ct' ;
2017-10-09 00:33:54 +03:00
if ( $this -> oContext -> sqlCountryList ) {
2017-10-07 13:01:56 +03:00
$sSQL .= ' JOIN placex USING (place_id)' ;
}
2017-10-08 22:23:31 +03:00
if ( $this -> oContext -> hasNearPoint ()) {
$sSQL .= ' WHERE ' . $this -> oContext -> withinSQL ( 'ct.centroid' );
2017-10-10 00:12:13 +03:00
} elseif ( $this -> oContext -> bViewboxBounded ) {
2017-10-08 23:44:01 +03:00
$sSQL .= ' WHERE ST_Contains(' . $this -> oContext -> sqlViewboxSmall . ', ct.centroid)' ;
2017-10-07 13:01:56 +03:00
}
2017-10-09 00:33:54 +03:00
if ( $this -> oContext -> sqlCountryList ) {
$sSQL .= ' AND country_code in ' . $this -> oContext -> sqlCountryList ;
2017-10-07 13:01:56 +03:00
}
2017-10-09 00:15:06 +03:00
$sSQL .= $this -> oContext -> excludeSQL ( ' AND place_id' );
2017-10-08 23:44:01 +03:00
if ( $this -> oContext -> sqlViewboxCentre ) {
$sSQL .= ' ORDER BY ST_Distance(' ;
$sSQL .= $this -> oContext -> sqlViewboxCentre . ', ct.centroid) ASC' ;
2017-10-08 22:23:31 +03:00
} elseif ( $this -> oContext -> hasNearPoint ()) {
$sSQL .= ' ORDER BY ' . $this -> oContext -> distanceSQL ( 'ct.centroid' ) . ' ASC' ;
2017-10-07 13:01:56 +03:00
}
2019-03-10 17:42:58 +03:00
$sSQL .= " LIMIT $iLimit " ;
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2019-03-10 17:42:58 +03:00
$aDBResults = $oDB -> getCol ( $sSQL );
2017-10-07 13:01:56 +03:00
}
2017-10-08 22:23:31 +03:00
if ( $this -> oContext -> hasNearPoint ()) {
2017-10-07 13:01:56 +03:00
$sSQL = 'SELECT place_id FROM placex WHERE ' ;
2019-03-10 17:42:58 +03:00
$sSQL .= 'class = :class and type = :type' ;
2017-10-08 22:23:31 +03:00
$sSQL .= ' AND ' . $this -> oContext -> withinSQL ( 'geometry' );
2017-10-07 13:01:56 +03:00
$sSQL .= ' AND linked_place_id is null' ;
2017-10-09 00:33:54 +03:00
if ( $this -> oContext -> sqlCountryList ) {
$sSQL .= ' AND country_code in ' . $this -> oContext -> sqlCountryList ;
2017-10-07 13:01:56 +03:00
}
2017-10-26 22:21:21 +03:00
$sSQL .= ' ORDER BY ' . $this -> oContext -> distanceSQL ( 'centroid' ) . ' ASC' ;
2017-10-07 13:01:56 +03:00
$sSQL .= " LIMIT $iLimit " ;
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2019-03-10 17:42:58 +03:00
$aDBResults = $oDB -> getCol (
$sSQL ,
array ( ':class' => $this -> sClass , ':type' => $this -> sType )
);
2017-10-07 13:01:56 +03:00
}
2017-10-19 00:54:47 +03:00
$aResults = array ();
foreach ( $aDBResults as $iPlaceId ) {
$aResults [ $iPlaceId ] = new Result ( $iPlaceId );
}
return $aResults ;
2017-10-07 13:01:56 +03:00
}
2017-10-09 23:55:50 +03:00
private function queryPostcode ( & $oDB , $iLimit )
2017-10-07 13:01:56 +03:00
{
2017-10-08 18:00:59 +03:00
$sSQL = 'SELECT p.place_id FROM location_postcode p ' ;
2017-10-07 13:01:56 +03:00
2018-03-22 14:36:24 +03:00
if ( ! empty ( $this -> aAddress )) {
2017-10-07 13:01:56 +03:00
$sSQL .= ', search_name s ' ;
$sSQL .= 'WHERE s.place_id = p.parent_place_id ' ;
$sSQL .= 'AND array_cat(s.nameaddress_vector, s.name_vector)' ;
2019-02-24 18:14:36 +03:00
$sSQL .= ' @> ' . $oDB -> getArraySQL ( $this -> aAddress ) . ' AND ' ;
2017-10-07 13:01:56 +03:00
} else {
$sSQL .= 'WHERE ' ;
}
2017-10-08 18:36:38 +03:00
$sSQL .= " p.postcode = ' " . reset ( $this -> aName ) . " ' " ;
2017-10-09 00:33:54 +03:00
$sSQL .= $this -> countryCodeSQL ( ' AND p.country_code' );
2019-04-02 15:49:31 +03:00
if ( $this -> oContext -> bViewboxBounded ) {
$sSQL .= ' AND ST_Intersects(' . $this -> oContext -> sqlViewboxSmall . ', geometry)' ;
}
2017-10-09 00:15:06 +03:00
$sSQL .= $this -> oContext -> excludeSQL ( ' AND p.place_id' );
2017-10-07 13:01:56 +03:00
$sSQL .= " LIMIT $iLimit " ;
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2017-10-07 13:01:56 +03:00
2017-10-19 00:54:47 +03:00
$aResults = array ();
2019-03-10 17:42:58 +03:00
foreach ( $oDB -> getCol ( $sSQL ) as $iPlaceId ) {
2017-10-19 00:54:47 +03:00
$aResults [ $iPlaceId ] = new Result ( $iPlaceId , Result :: TABLE_POSTCODE );
}
return $aResults ;
2017-10-07 13:01:56 +03:00
}
2018-05-06 23:10:38 +03:00
private function queryNamedPlace ( & $oDB , $iMinAddressRank , $iMaxAddressRank , $iLimit )
2017-10-07 13:01:56 +03:00
{
$aTerms = array ();
$aOrder = array ();
2018-05-07 22:28:32 +03:00
// Sort by existence of the requested house number but only if not
// too many results are expected for the street, i.e. if the result
// will be narrowed down by an address. Remeber that with ordering
// every single result has to be checked.
2021-03-29 13:06:51 +03:00
if ( $this -> sHouseNumber && ( $this -> bRareName || ! empty ( $this -> aAddress ) || $this -> sPostcode )) {
2017-10-07 13:01:56 +03:00
$sHouseNumberRegex = '\\\\m' . $this -> sHouseNumber . '\\\\M' ;
$aOrder [] = ' (' ;
$aOrder [ 0 ] .= 'EXISTS(' ;
$aOrder [ 0 ] .= ' SELECT place_id' ;
$aOrder [ 0 ] .= ' FROM placex' ;
$aOrder [ 0 ] .= ' WHERE parent_place_id = search_name.place_id' ;
2021-03-29 17:45:09 +03:00
$aOrder [ 0 ] .= " AND housenumber ~* E' " . $sHouseNumberRegex . " ' " ;
2017-10-07 13:01:56 +03:00
$aOrder [ 0 ] .= ' LIMIT 1' ;
$aOrder [ 0 ] .= ') ' ;
// also housenumbers from interpolation lines table are needed
if ( preg_match ( '/[0-9]+/' , $this -> sHouseNumber )) {
$iHouseNumber = intval ( $this -> sHouseNumber );
$aOrder [ 0 ] .= 'OR EXISTS(' ;
$aOrder [ 0 ] .= ' SELECT place_id ' ;
$aOrder [ 0 ] .= ' FROM location_property_osmline ' ;
$aOrder [ 0 ] .= ' WHERE parent_place_id = search_name.place_id' ;
$aOrder [ 0 ] .= ' AND startnumber is not NULL' ;
$aOrder [ 0 ] .= ' AND ' . $iHouseNumber . '>=startnumber ' ;
$aOrder [ 0 ] .= ' AND ' . $iHouseNumber . '<=endnumber ' ;
$aOrder [ 0 ] .= ' LIMIT 1' ;
$aOrder [ 0 ] .= ')' ;
}
$aOrder [ 0 ] .= ') DESC' ;
}
2018-03-22 14:36:24 +03:00
if ( ! empty ( $this -> aName )) {
2019-02-24 18:14:36 +03:00
$aTerms [] = 'name_vector @> ' . $oDB -> getArraySQL ( $this -> aName );
2017-10-07 13:01:56 +03:00
}
2018-03-22 14:36:24 +03:00
if ( ! empty ( $this -> aAddress )) {
2017-10-07 13:01:56 +03:00
// For infrequent name terms disable index usage for address
2018-05-06 23:10:38 +03:00
if ( $this -> bRareName ) {
2019-02-24 18:14:36 +03:00
$aTerms [] = 'array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ' . $oDB -> getArraySQL ( $this -> aAddress );
2017-10-07 13:01:56 +03:00
} else {
2019-02-24 18:14:36 +03:00
$aTerms [] = 'nameaddress_vector @> ' . $oDB -> getArraySQL ( $this -> aAddress );
2017-10-07 13:01:56 +03:00
}
}
2017-10-09 00:33:54 +03:00
$sCountryTerm = $this -> countryCodeSQL ( 'country_code' );
2017-10-07 13:01:56 +03:00
if ( $sCountryTerm ) {
$aTerms [] = $sCountryTerm ;
}
if ( $this -> sHouseNumber ) {
2020-07-08 18:47:38 +03:00
$aTerms [] = 'address_rank between 16 and 30' ;
2017-10-07 13:01:56 +03:00
} elseif ( ! $this -> sClass || $this -> iOperator == Operator :: NAME ) {
if ( $iMinAddressRank > 0 ) {
2020-02-21 18:35:59 +03:00
$aTerms [] = " ((address_rank between $iMinAddressRank and $iMaxAddressRank ) or (search_rank between $iMinAddressRank and $iMaxAddressRank )) " ;
2017-10-07 13:01:56 +03:00
}
}
2017-10-08 22:23:31 +03:00
if ( $this -> oContext -> hasNearPoint ()) {
$aTerms [] = $this -> oContext -> withinSQL ( 'centroid' );
$aOrder [] = $this -> oContext -> distanceSQL ( 'centroid' );
2017-10-07 13:01:56 +03:00
} elseif ( $this -> sPostcode ) {
2018-03-22 14:36:24 +03:00
if ( empty ( $this -> aAddress )) {
2017-10-07 13:01:56 +03:00
$aTerms [] = " EXISTS(SELECT place_id FROM location_postcode p WHERE p.postcode = ' " . $this -> sPostcode . " ' AND ST_DWithin(search_name.centroid, p.geometry, 0.1)) " ;
} else {
$aOrder [] = " (SELECT min(ST_Distance(search_name.centroid, p.geometry)) FROM location_postcode p WHERE p.postcode = ' " . $this -> sPostcode . " ') " ;
}
}
2017-10-09 00:15:06 +03:00
$sExcludeSQL = $this -> oContext -> excludeSQL ( 'place_id' );
2017-10-07 13:01:56 +03:00
if ( $sExcludeSQL ) {
2017-10-09 00:15:06 +03:00
$aTerms [] = $sExcludeSQL ;
2017-10-07 13:01:56 +03:00
}
2017-10-08 23:44:01 +03:00
if ( $this -> oContext -> bViewboxBounded ) {
$aTerms [] = 'centroid && ' . $this -> oContext -> sqlViewboxSmall ;
2017-10-07 13:01:56 +03:00
}
2017-10-08 22:23:31 +03:00
if ( $this -> oContext -> hasNearPoint ()) {
$aOrder [] = $this -> oContext -> distanceSQL ( 'centroid' );
2017-10-07 13:01:56 +03:00
}
if ( $this -> sHouseNumber ) {
$sImportanceSQL = '- abs(26 - address_rank) + 3' ;
} else {
2018-02-20 01:24:12 +03:00
$sImportanceSQL = '(CASE WHEN importance = 0 OR importance IS NULL THEN 0.75001-(search_rank::float/40) ELSE importance END)' ;
2017-10-07 13:01:56 +03:00
}
2017-10-08 23:44:01 +03:00
$sImportanceSQL .= $this -> oContext -> viewboxImportanceSQL ( 'centroid' );
2017-10-07 13:01:56 +03:00
$aOrder [] = " $sImportanceSQL DESC " ;
2020-11-25 13:44:25 +03:00
$aFullNameAddress = $this -> oContext -> getFullNameTerms ();
if ( ! empty ( $aFullNameAddress )) {
2017-10-07 13:01:56 +03:00
$sExactMatchSQL = ' ( ' ;
2017-10-08 11:06:17 +03:00
$sExactMatchSQL .= ' SELECT count(*) FROM ( ' ;
2020-11-25 13:44:25 +03:00
$sExactMatchSQL .= ' SELECT unnest(' . $oDB -> getArraySQL ( $aFullNameAddress ) . ')' ;
2017-10-08 11:06:17 +03:00
$sExactMatchSQL .= ' INTERSECT ' ;
$sExactMatchSQL .= ' SELECT unnest(nameaddress_vector)' ;
$sExactMatchSQL .= ' ) s' ;
2017-10-07 13:01:56 +03:00
$sExactMatchSQL .= ') as exactmatch' ;
$aOrder [] = 'exactmatch DESC' ;
} else {
$sExactMatchSQL = '0::int as exactmatch' ;
}
if ( $this -> sHouseNumber || $this -> sClass ) {
2018-10-20 18:26:45 +03:00
$iLimit = 40 ;
2017-10-07 13:01:56 +03:00
}
2017-10-19 00:54:47 +03:00
$aResults = array ();
2018-03-22 14:36:24 +03:00
if ( ! empty ( $aTerms )) {
2021-02-16 19:47:06 +03:00
$sSQL = 'SELECT place_id, address_rank,' . $sExactMatchSQL ;
2017-10-07 13:01:56 +03:00
$sSQL .= ' FROM search_name' ;
$sSQL .= ' WHERE ' . join ( ' and ' , $aTerms );
$sSQL .= ' ORDER BY ' . join ( ', ' , $aOrder );
$sSQL .= ' LIMIT ' . $iLimit ;
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2017-10-07 13:01:56 +03:00
2019-03-10 17:42:58 +03:00
$aDBResults = $oDB -> getAll ( $sSQL , null , 'Could not get places for search terms.' );
2017-10-19 00:54:47 +03:00
foreach ( $aDBResults as $aResult ) {
$oResult = new Result ( $aResult [ 'place_id' ]);
$oResult -> iExactMatches = $aResult [ 'exactmatch' ];
2021-02-16 19:47:06 +03:00
$oResult -> iAddressRank = $aResult [ 'address_rank' ];
2017-10-19 00:54:47 +03:00
$aResults [ $aResult [ 'place_id' ]] = $oResult ;
}
2017-10-07 13:01:56 +03:00
}
2017-10-19 00:54:47 +03:00
return $aResults ;
2017-10-07 13:01:56 +03:00
}
2018-04-06 23:20:21 +03:00
private function queryHouseNumber ( & $oDB , $aRoadPlaceIDs )
2017-10-07 13:01:56 +03:00
{
2017-10-19 00:54:47 +03:00
$aResults = array ();
2021-06-17 13:05:33 +03:00
$sRoadPlaceIDs = Result :: joinIdsByTableMaxRank (
$aRoadPlaceIDs ,
Result :: TABLE_PLACEX ,
27
);
$sPOIPlaceIDs = Result :: joinIdsByTableMinRank (
$aRoadPlaceIDs ,
Result :: TABLE_PLACEX ,
30
);
$aIDCondition = array ();
if ( $sRoadPlaceIDs ) {
$aIDCondition [] = 'parent_place_id in (' . $sRoadPlaceIDs . ')' ;
}
if ( $sPOIPlaceIDs ) {
$aIDCondition [] = 'place_id in (' . $sPOIPlaceIDs . ')' ;
}
2017-10-19 00:54:47 +03:00
2021-06-17 13:05:33 +03:00
if ( empty ( $aIDCondition )) {
2017-10-19 00:54:47 +03:00
return $aResults ;
}
2017-10-07 13:01:56 +03:00
$sHouseNumberRegex = '\\\\m' . $this -> sHouseNumber . '\\\\M' ;
2021-06-17 13:05:33 +03:00
$sSQL = 'SELECT place_id FROM placex WHERE' ;
$sSQL .= " housenumber ~* E' " . $sHouseNumberRegex . " ' " ;
$sSQL .= ' AND (' . join ( ' OR ' , $aIDCondition ) . ')' ;
2017-10-09 00:15:06 +03:00
$sSQL .= $this -> oContext -> excludeSQL ( ' AND place_id' );
2017-10-07 13:01:56 +03:00
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2017-10-07 13:01:56 +03:00
2017-10-19 00:54:47 +03:00
// XXX should inherit the exactMatches from its parent
2019-03-10 17:42:58 +03:00
foreach ( $oDB -> getCol ( $sSQL ) as $iPlaceId ) {
2017-10-19 00:54:47 +03:00
$aResults [ $iPlaceId ] = new Result ( $iPlaceId );
2017-10-07 13:01:56 +03:00
}
$bIsIntHouseNumber = ( bool ) preg_match ( '/[0-9]+/' , $this -> sHouseNumber );
$iHousenumber = intval ( $this -> sHouseNumber );
2021-06-17 13:05:33 +03:00
if ( $bIsIntHouseNumber && $sRoadPlaceIDs && empty ( $aResults )) {
2017-10-07 13:01:56 +03:00
// if nothing found, search in the interpolation line table
$sSQL = 'SELECT distinct place_id FROM location_property_osmline' ;
$sSQL .= ' WHERE startnumber is not NULL' ;
2021-06-17 13:05:33 +03:00
$sSQL .= ' AND parent_place_id in (' . $sRoadPlaceIDs . ') AND (' ;
2017-10-07 13:01:56 +03:00
if ( $iHousenumber % 2 == 0 ) {
// If housenumber is even, look for housenumber in streets
// with interpolationtype even or all.
$sSQL .= " interpolationtype='even' " ;
} else {
// Else look for housenumber with interpolationtype odd or all.
$sSQL .= " interpolationtype='odd' " ;
}
$sSQL .= " or interpolationtype='all') and " ;
2017-10-26 22:21:21 +03:00
$sSQL .= $iHousenumber . '>=startnumber and ' ;
$sSQL .= $iHousenumber . '<=endnumber' ;
2017-10-09 00:15:06 +03:00
$sSQL .= $this -> oContext -> excludeSQL ( ' AND place_id' );
2017-10-07 13:01:56 +03:00
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2017-10-07 13:01:56 +03:00
2019-03-10 17:42:58 +03:00
foreach ( $oDB -> getCol ( $sSQL ) as $iPlaceId ) {
2017-10-19 00:54:47 +03:00
$oResult = new Result ( $iPlaceId , Result :: TABLE_OSMLINE );
$oResult -> iHouseNumber = $iHousenumber ;
$aResults [ $iPlaceId ] = $oResult ;
2017-10-07 13:01:56 +03:00
}
}
// If nothing found then search in Tiger data (location_property_tiger)
2021-06-17 13:05:33 +03:00
if ( CONST_Use_US_Tiger_Data && $sRoadPlaceIDs && $bIsIntHouseNumber && empty ( $aResults )) {
2017-10-19 00:54:47 +03:00
$sSQL = 'SELECT place_id FROM location_property_tiger' ;
2021-06-17 13:05:33 +03:00
$sSQL .= ' WHERE parent_place_id in (' . $sRoadPlaceIDs . ') and (' ;
2017-10-07 13:01:56 +03:00
if ( $iHousenumber % 2 == 0 ) {
$sSQL .= " interpolationtype='even' " ;
} else {
$sSQL .= " interpolationtype='odd' " ;
}
$sSQL .= " or interpolationtype='all') and " ;
2017-10-26 22:21:21 +03:00
$sSQL .= $iHousenumber . '>=startnumber and ' ;
$sSQL .= $iHousenumber . '<=endnumber' ;
2017-10-09 00:15:06 +03:00
$sSQL .= $this -> oContext -> excludeSQL ( ' AND place_id' );
2017-10-07 13:01:56 +03:00
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2017-10-07 13:01:56 +03:00
2019-03-10 17:42:58 +03:00
foreach ( $oDB -> getCol ( $sSQL ) as $iPlaceId ) {
2017-10-19 00:54:47 +03:00
$oResult = new Result ( $iPlaceId , Result :: TABLE_TIGER );
$oResult -> iHouseNumber = $iHousenumber ;
$aResults [ $iPlaceId ] = $oResult ;
2017-10-07 13:01:56 +03:00
}
}
2017-10-19 00:54:47 +03:00
return $aResults ;
2017-10-07 13:01:56 +03:00
}
2017-10-09 23:55:50 +03:00
private function queryPoiByOperator ( & $oDB , $aParentIDs , $iLimit )
2017-10-07 13:01:56 +03:00
{
2017-10-19 00:54:47 +03:00
$aResults = array ();
$sPlaceIDs = Result :: joinIdsByTable ( $aParentIDs , Result :: TABLE_PLACEX );
if ( ! $sPlaceIDs ) {
return $aResults ;
}
2017-10-07 13:01:56 +03:00
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 . " ' " ;
2017-10-26 22:21:21 +03:00
$sSQL .= ' AND linked_place_id is null' ;
2017-10-09 00:15:06 +03:00
$sSQL .= $this -> oContext -> excludeSQL ( ' AND place_id' );
2017-10-26 22:21:21 +03:00
$sSQL .= ' ORDER BY rank_search ASC ' ;
2017-10-07 13:01:56 +03:00
$sSQL .= " LIMIT $iLimit " ;
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2017-10-07 13:01:56 +03:00
2019-03-10 17:42:58 +03:00
foreach ( $oDB -> getCol ( $sSQL ) as $iPlaceId ) {
2017-10-19 00:54:47 +03:00
$aResults [ $iPlaceId ] = new Result ( $iPlaceId );
}
2017-10-07 13:01:56 +03:00
}
// NEAR and IN are handled the same
if ( $this -> iOperator == Operator :: TYPE || $this -> iOperator == Operator :: NEAR ) {
$sClassTable = $this -> poiTable ();
2019-03-10 17:42:58 +03:00
$bCacheTable = $oDB -> tableExists ( $sClassTable );
2017-10-07 13:01:56 +03:00
$sSQL = " SELECT min(rank_search) FROM placex WHERE place_id in ( $sPlaceIDs ) " ;
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2019-03-10 17:42:58 +03:00
$iMaxRank = ( int ) $oDB -> getOne ( $sSQL );
2017-10-07 13:01:56 +03:00
// 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_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon') " ;
2017-10-26 22:21:21 +03:00
$sSQL .= ' ORDER BY rank_search ASC ' ;
$sSQL .= ' LIMIT 1' ;
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2019-03-10 17:42:58 +03:00
$sPlaceGeom = $oDB -> getOne ( $sSQL );
2017-10-07 13:01:56 +03:00
}
if ( $sPlaceGeom ) {
$sPlaceIDs = false ;
} else {
$iMaxRank += 5 ;
$sSQL = 'SELECT place_id FROM placex' ;
$sSQL .= " WHERE place_id in ( $sPlaceIDs ) and rank_search < $iMaxRank " ;
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2019-03-10 17:42:58 +03:00
$aPlaceIDs = $oDB -> getCol ( $sSQL );
2017-10-07 13:01:56 +03:00
$sPlaceIDs = join ( ',' , $aPlaceIDs );
}
if ( $sPlaceIDs || $sPlaceGeom ) {
$fRange = 0.01 ;
if ( $bCacheTable ) {
// More efficient - can make the range bigger
$fRange = 0.05 ;
$sOrderBySQL = '' ;
2017-10-08 22:23:31 +03:00
if ( $this -> oContext -> hasNearPoint ()) {
$sOrderBySQL = $this -> oContext -> distanceSQL ( 'l.centroid' );
2017-10-07 13:01:56 +03:00
} elseif ( $sPlaceIDs ) {
2017-10-26 22:21:21 +03:00
$sOrderBySQL = 'ST_Distance(l.centroid, f.geometry)' ;
2017-10-07 13:01:56 +03:00
} 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 ) {
2017-10-26 22:21:21 +03:00
$sSQL .= ',placex as f WHERE ' ;
2017-10-07 13:01:56 +03:00
$sSQL .= " f.place_id in ( $sPlaceIDs ) " ;
$sSQL .= " AND ST_DWithin(l.centroid, f.centroid, $fRange ) " ;
} elseif ( $sPlaceGeom ) {
$sSQL .= " WHERE ST_Contains(' $sPlaceGeom ', l.centroid) " ;
}
2017-10-09 00:15:06 +03:00
$sSQL .= $this -> oContext -> excludeSQL ( ' AND l.place_id' );
2017-10-07 13:01:56 +03:00
$sSQL .= 'limit 300) i ' ;
if ( $sOrderBySQL ) {
$sSQL .= 'order by order_term asc' ;
}
$sSQL .= " limit $iLimit " ;
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2017-10-07 13:01:56 +03:00
2019-03-10 17:42:58 +03:00
foreach ( $oDB -> getCol ( $sSQL ) as $iPlaceId ) {
2017-10-19 00:54:47 +03:00
$aResults [ $iPlaceId ] = new Result ( $iPlaceId );
}
2017-10-07 13:01:56 +03:00
} else {
2017-10-08 22:23:31 +03:00
if ( $this -> oContext -> hasNearPoint ()) {
$fRange = $this -> oContext -> nearRadius ();
2017-10-07 13:01:56 +03:00
}
$sOrderBySQL = '' ;
2017-10-08 22:23:31 +03:00
if ( $this -> oContext -> hasNearPoint ()) {
$sOrderBySQL = $this -> oContext -> distanceSQL ( 'l.geometry' );
2017-10-07 13:01:56 +03:00
} else {
2017-10-26 22:21:21 +03:00
$sOrderBySQL = 'ST_Distance(l.geometry, f.geometry)' ;
2017-10-07 13:01:56 +03:00
}
$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 . " ' " ;
2017-10-09 00:15:06 +03:00
$sSQL .= $this -> oContext -> excludeSQL ( ' AND l.place_id' );
2017-10-07 13:01:56 +03:00
if ( $sOrderBySQL ) {
2017-10-26 22:21:21 +03:00
$sSQL .= 'ORDER BY orderterm ASC' ;
2017-10-07 13:01:56 +03:00
}
$sSQL .= " limit $iLimit " ;
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2017-10-07 13:01:56 +03:00
2019-03-10 17:42:58 +03:00
foreach ( $oDB -> getCol ( $sSQL ) as $iPlaceId ) {
2017-10-19 00:54:47 +03:00
$aResults [ $iPlaceId ] = new Result ( $iPlaceId );
}
2017-10-07 13:01:56 +03:00
}
}
}
2017-10-19 00:54:47 +03:00
return $aResults ;
2017-10-07 13:01:56 +03:00
}
2017-10-08 16:26:14 +03:00
2017-10-10 00:12:13 +03:00
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 '' ;
}
2017-10-08 16:26:14 +03:00
/////////// Sort functions
2017-10-08 18:00:59 +03:00
public static function bySearchRank ( $a , $b )
2017-10-08 16:26:14 +03:00
{
if ( $a -> iSearchRank == $b -> iSearchRank ) {
return $a -> iOperator + strlen ( $a -> sHouseNumber )
- $b -> iOperator - strlen ( $b -> sHouseNumber );
}
return $a -> iSearchRank < $b -> iSearchRank ? - 1 : 1 ;
}
2017-10-08 17:03:30 +03:00
//////////// Debugging functions
2017-10-08 18:00:59 +03:00
2018-03-24 19:44:13 +03:00
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 ,
2021-01-17 00:20:23 +03:00
'Address terms (full words)' => $this -> aFullNameAddress ? ? '' ,
2018-03-24 19:44:13 +03:00
'Special search' => $this -> iOperator ,
'Class' => $this -> sClass ,
'Type' => $this -> sType ,
'House number' => $this -> sHouseNumber ,
'Postcode' => $this -> sPostcode
);
}
2017-10-08 18:00:59 +03:00
public function dumpAsHtmlTableRow ( & $aWordIDs )
2017-10-08 17:03:30 +03:00
{
2017-10-08 18:00:59 +03:00
$kf = function ( $k ) use ( & $aWordIDs ) {
2021-01-17 00:20:23 +03:00
return $aWordIDs [ $k ] ? ? '[' . $k . ']' ;
2017-10-08 18:00:59 +03:00
};
2017-10-08 17:03:30 +03:00
2017-10-26 22:21:21 +03:00
echo '<tr>' ;
2017-10-08 17:03:30 +03:00
echo " <td> $this->iSearchRank </td> " ;
2017-10-26 22:21:21 +03:00
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>' ;
2017-10-08 17:03:30 +03:00
}
2017-10-08 18:00:59 +03:00
}