2013-08-27 20:08:57 +04:00
< ? php
2016-09-16 03:27:36 +03:00
namespace Nominatim ;
2016-09-04 04:19:48 +03:00
require_once ( CONST_BasePath . '/lib/PlaceLookup.php' );
2017-10-12 23:37:44 +03:00
require_once ( CONST_BasePath . '/lib/Phrase.php' );
2016-09-04 04:19:48 +03:00
require_once ( CONST_BasePath . '/lib/ReverseGeocode.php' );
2017-10-08 16:26:14 +03:00
require_once ( CONST_BasePath . '/lib/SearchDescription.php' );
2017-10-08 22:23:31 +03:00
require_once ( CONST_BasePath . '/lib/SearchContext.php' );
2018-05-15 00:04:15 +03:00
require_once ( CONST_BasePath . '/lib/TokenList.php' );
2016-09-04 04:19:48 +03:00
class Geocode
{
protected $oDB ;
2017-10-24 00:28:00 +03:00
protected $oPlaceLookup ;
2016-09-04 04:19:48 +03:00
protected $aLangPrefOrder = array ();
protected $bIncludeAddressDetails = false ;
protected $aExcludePlaceIDs = array ();
protected $bReverseInPlan = false ;
protected $iLimit = 20 ;
protected $iFinalLimit = 10 ;
protected $iOffset = 0 ;
protected $bFallback = false ;
protected $aCountryCodes = false ;
protected $bBoundedSearch = false ;
protected $aViewBox = false ;
2017-10-08 23:44:01 +03:00
protected $aRoutePoints = false ;
protected $aRouteWidth = false ;
2016-09-04 04:19:48 +03:00
protected $iMaxRank = 20 ;
protected $iMinAddressRank = 0 ;
protected $iMaxAddressRank = 30 ;
protected $aAddressRankList = array ();
protected $sAllowedTypesSQLList = false ;
protected $sQuery = false ;
protected $aStructuredQuery = false ;
2017-07-07 00:06:13 +03:00
protected $oNormalizer = null ;
2016-09-14 04:16:46 +03:00
2016-09-16 03:27:36 +03:00
public function __construct ( & $oDB )
2016-09-04 04:19:48 +03:00
{
$this -> oDB =& $oDB ;
2017-10-24 00:28:00 +03:00
$this -> oPlaceLookup = new PlaceLookup ( $this -> oDB );
2017-07-07 00:06:13 +03:00
$this -> oNormalizer = \Transliterator :: createFromRules ( CONST_Term_Normalization_Rules );
}
private function normTerm ( $sTerm )
{
if ( $this -> oNormalizer === null ) {
2017-09-30 10:39:47 +03:00
return $sTerm ;
2017-07-07 00:06:13 +03:00
}
return $this -> oNormalizer -> transliterate ( $sTerm );
2016-09-04 04:19:48 +03:00
}
2016-09-16 03:27:36 +03:00
public function setReverseInPlan ( $bReverse )
2016-09-04 04:19:48 +03:00
{
$this -> bReverseInPlan = $bReverse ;
}
2016-09-16 03:27:36 +03:00
public function setLanguagePreference ( $aLangPref )
2016-09-04 04:19:48 +03:00
{
$this -> aLangPrefOrder = $aLangPref ;
}
2017-03-23 02:16:58 +03:00
public function getMoreUrlParams ()
2016-09-04 04:19:48 +03:00
{
2017-03-23 02:16:58 +03:00
if ( $this -> aStructuredQuery ) {
$aParams = $this -> aStructuredQuery ;
} else {
$aParams = array ( 'q' => $this -> sQuery );
}
2016-09-04 04:19:48 +03:00
2017-10-24 00:28:00 +03:00
$aParams = array_merge ( $aParams , $this -> oPlaceLookup -> getMoreUrlParams ());
2017-03-23 02:16:58 +03:00
if ( $this -> aExcludePlaceIDs ) {
$aParams [ 'exclude_place_ids' ] = implode ( ',' , $this -> aExcludePlaceIDs );
}
2016-09-04 04:19:48 +03:00
2017-03-23 02:16:58 +03:00
if ( $this -> bIncludeAddressDetails ) $aParams [ 'addressdetails' ] = '1' ;
if ( $this -> bBoundedSearch ) $aParams [ 'bounded' ] = '1' ;
if ( $this -> aCountryCodes ) {
$aParams [ 'countrycodes' ] = implode ( ',' , $this -> aCountryCodes );
}
if ( $this -> aViewBox ) {
2017-10-22 22:59:08 +03:00
$aParams [ 'viewbox' ] = join ( ',' , $this -> aViewBox );
2017-03-23 02:16:58 +03:00
}
return $aParams ;
2016-09-04 04:19:48 +03:00
}
2016-09-16 03:27:36 +03:00
public function setLimit ( $iLimit = 10 )
2016-09-04 04:19:48 +03:00
{
if ( $iLimit > 50 ) $iLimit = 50 ;
if ( $iLimit < 1 ) $iLimit = 1 ;
$this -> iFinalLimit = $iLimit ;
$this -> iLimit = $iLimit + min ( $iLimit , 10 );
}
2016-09-16 03:27:36 +03:00
public function setFeatureType ( $sFeatureType )
2016-09-04 04:19:48 +03:00
{
2016-09-08 04:16:22 +03:00
switch ( $sFeatureType ) {
2016-09-14 04:16:46 +03:00
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 ;
2016-09-04 04:19:48 +03:00
}
}
2016-09-16 03:27:36 +03:00
public function setRankRange ( $iMin , $iMax )
2016-09-04 04:19:48 +03:00
{
$this -> iMinAddressRank = $iMin ;
$this -> iMaxAddressRank = $iMax ;
}
2016-09-16 03:27:36 +03:00
public function setViewbox ( $aViewbox )
2016-09-04 04:19:48 +03:00
{
2017-10-22 22:59:08 +03:00
$aBox = array_map ( 'floatval' , $aViewbox );
2016-09-04 04:19:48 +03:00
2017-10-22 22:59:08 +03:00
$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 ]));
2016-10-13 09:03:28 +03:00
2017-10-22 22:59:08 +03:00
if ( $this -> aViewBox [ 2 ] - $this -> aViewBox [ 0 ] < 0.000000001
|| $this -> aViewBox [ 3 ] - $this -> aViewBox [ 1 ] < 0.000000001
2016-10-12 23:13:07 +03:00
) {
2016-10-13 09:03:28 +03:00
userError ( " Bad parameter 'viewbox'. Not a box. " );
2016-10-12 23:13:07 +03:00
}
2016-09-04 04:19:48 +03:00
}
2017-10-22 22:59:08 +03:00
private function viewboxImportanceFactor ( $fX , $fY )
{
$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 ;
}
2016-09-16 03:27:36 +03:00
public function setQuery ( $sQueryString )
2016-09-04 04:19:48 +03:00
{
$this -> sQuery = $sQueryString ;
$this -> aStructuredQuery = false ;
}
2016-09-16 03:27:36 +03:00
public function getQueryString ()
2016-09-04 04:19:48 +03:00
{
return $this -> sQuery ;
}
2017-10-24 00:28:00 +03:00
public function loadParamArray ( $oParams , $sForceGeometryType = null )
2016-09-04 04:19:48 +03:00
{
2016-09-11 06:22:51 +03:00
$this -> bIncludeAddressDetails
= $oParams -> getBool ( 'addressdetails' , $this -> bIncludeAddressDetails );
2016-09-04 04:19:48 +03:00
$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 acurate pageing
$sExcluded = $oParams -> getStringList ( 'exclude_place_ids' );
2016-09-08 04:16:22 +03:00
if ( $sExcluded ) {
foreach ( $sExcluded as $iExcludedPlaceID ) {
2016-09-04 04:19:48 +03:00
$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' );
2016-09-08 04:16:22 +03:00
if ( $sCountries ) {
foreach ( $sCountries as $sCountryCode ) {
if ( preg_match ( '/^[a-zA-Z][a-zA-Z]$/' , $sCountryCode )) {
2016-09-04 04:19:48 +03:00
$aCountries [] = strtolower ( $sCountryCode );
}
}
2016-10-04 21:32:22 +03:00
if ( isset ( $aCountries ))
2016-09-04 04:19:48 +03:00
$this -> aCountryCodes = $aCountries ;
}
$aViewbox = $oParams -> getStringList ( 'viewboxlbrt' );
2016-09-08 04:16:22 +03:00
if ( $aViewbox ) {
2016-10-12 23:13:07 +03:00
if ( count ( $aViewbox ) != 4 ) {
2018-02-08 20:02:19 +03:00
userError ( " Bad parameter 'viewboxlbrt'. Expected 4 coordinates. " );
2016-10-12 23:13:07 +03:00
}
2016-09-04 04:19:48 +03:00
$this -> setViewbox ( $aViewbox );
2016-09-08 04:16:22 +03:00
} else {
2016-09-04 04:19:48 +03:00
$aViewbox = $oParams -> getStringList ( 'viewbox' );
2016-09-08 04:16:22 +03:00
if ( $aViewbox ) {
2016-10-12 23:13:07 +03:00
if ( count ( $aViewbox ) != 4 ) {
2018-02-08 20:02:19 +03:00
userError ( " Bad parameter 'viewbox'. Expected 4 coordinates. " );
2016-10-12 23:13:07 +03:00
}
2017-09-30 15:16:07 +03:00
$this -> setViewBox ( $aViewbox );
2016-09-08 04:16:22 +03:00
} else {
2016-09-04 04:19:48 +03:00
$aRoute = $oParams -> getStringList ( 'route' );
$fRouteWidth = $oParams -> getFloat ( 'routewidth' );
2016-09-08 04:16:22 +03:00
if ( $aRoute && $fRouteWidth ) {
2017-10-08 23:44:01 +03:00
$this -> aRoutePoints = $aRoute ;
$this -> aRouteWidth = $fRouteWidth ;
2016-09-04 04:19:48 +03:00
}
}
}
2017-10-24 00:28:00 +03:00
$this -> oPlaceLookup -> loadParamArray ( $oParams , $sForceGeometryType );
$this -> oPlaceLookup -> setIncludeAddressDetails ( false );
$this -> oPlaceLookup -> setIncludePolygonAsPoints ( $oParams -> getBool ( 'polygon' ));
2018-03-18 03:49:26 +03:00
2018-07-06 22:26:54 +03:00
if ( $this -> bIncludeAddressDetails
&& $oParams -> getString ( 'format' , '' ) == 'geocodejson'
) {
2018-03-18 03:49:26 +03:00
$this -> oPlaceLookup -> setAddressAdminLevels ( true );
}
2016-09-04 04:19:48 +03:00
}
2016-09-16 03:27:36 +03:00
public function setQueryFromParams ( $oParams )
2016-09-04 04:19:48 +03:00
{
// Search query
$sQuery = $oParams -> getString ( 'q' );
2016-09-08 04:16:22 +03:00
if ( ! $sQuery ) {
2016-09-11 06:22:51 +03:00
$this -> setStructuredQuery (
$oParams -> getString ( 'amenity' ),
$oParams -> getString ( 'street' ),
$oParams -> getString ( 'city' ),
$oParams -> getString ( 'county' ),
$oParams -> getString ( 'state' ),
$oParams -> getString ( 'country' ),
$oParams -> getString ( 'postalcode' )
);
2016-09-04 04:19:48 +03:00
$this -> setReverseInPlan ( false );
2016-09-08 04:16:22 +03:00
} else {
2016-09-04 04:19:48 +03:00
$this -> setQuery ( $sQuery );
}
}
2016-09-16 03:27:36 +03:00
public function loadStructuredAddressElement ( $sValue , $sKey , $iNewMinAddressRank , $iNewMaxAddressRank , $aItemListValues )
2016-09-04 04:19:48 +03:00
{
$sValue = trim ( $sValue );
if ( ! $sValue ) return false ;
$this -> aStructuredQuery [ $sKey ] = $sValue ;
2016-09-08 04:16:22 +03:00
if ( $this -> iMinAddressRank == 0 && $this -> iMaxAddressRank == 30 ) {
2016-09-04 04:19:48 +03:00
$this -> iMinAddressRank = $iNewMinAddressRank ;
$this -> iMaxAddressRank = $iNewMaxAddressRank ;
}
if ( $aItemListValues ) $this -> aAddressRankList = array_merge ( $this -> aAddressRankList , $aItemListValues );
return true ;
}
2017-03-23 02:16:58 +03:00
public function setStructuredQuery ( $sAmenity = false , $sStreet = false , $sCity = false , $sCounty = false , $sState = false , $sCountry = false , $sPostalCode = false )
2016-09-04 04:19:48 +03:00
{
$this -> sQuery = false ;
// Reset
$this -> iMinAddressRank = 0 ;
$this -> iMaxAddressRank = 30 ;
$this -> aAddressRankList = array ();
$this -> aStructuredQuery = array ();
2017-10-08 18:00:59 +03:00
$this -> sAllowedTypesSQLList = false ;
2016-09-04 04:19:48 +03:00
2017-03-23 02:16:58 +03:00
$this -> loadStructuredAddressElement ( $sAmenity , 'amenity' , 26 , 30 , false );
2016-09-04 04:19:48 +03:00
$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 );
2016-09-11 06:22:51 +03:00
$this -> loadStructuredAddressElement ( $sPostalCode , 'postalcode' , 5 , 11 , array ( 5 , 11 ));
2016-09-04 04:19:48 +03:00
$this -> loadStructuredAddressElement ( $sCountry , 'country' , 4 , 4 , false );
2018-03-22 14:36:24 +03:00
if ( ! empty ( $this -> aStructuredQuery )) {
2016-09-04 04:19:48 +03:00
$this -> sQuery = join ( ', ' , $this -> aStructuredQuery );
2016-09-08 04:16:22 +03:00
if ( $this -> iMaxAddressRank < 30 ) {
2017-09-18 00:30:08 +03:00
$this -> sAllowedTypesSQLList = '(\'place\',\'boundary\')' ;
2016-09-04 04:19:48 +03:00
}
}
}
2016-09-16 03:27:36 +03:00
public function fallbackStructuredQuery ()
2016-09-04 04:19:48 +03:00
{
if ( ! $this -> aStructuredQuery ) return false ;
$aParams = $this -> aStructuredQuery ;
2018-03-22 14:36:24 +03:00
if ( count ( $aParams ) == 1 ) return false ;
2016-09-04 04:19:48 +03:00
$aOrderToFallback = array ( 'postalcode' , 'street' , 'city' , 'county' , 'state' );
2016-09-08 04:16:22 +03:00
foreach ( $aOrderToFallback as $sType ) {
if ( isset ( $aParams [ $sType ])) {
2016-09-04 04:19:48 +03:00
unset ( $aParams [ $sType ]);
$this -> setStructuredQuery ( @ $aParams [ 'amenity' ], @ $aParams [ 'street' ], @ $aParams [ 'city' ], @ $aParams [ 'county' ], @ $aParams [ 'state' ], @ $aParams [ 'country' ], @ $aParams [ 'postalcode' ]);
return true ;
}
}
return false ;
}
2018-05-15 00:04:15 +03:00
public function getGroupedSearches ( $aSearches , $aPhrases , $oValidTokens , $bIsStructured )
2016-09-04 04:19:48 +03:00
{
/*
2018-05-15 00:04:15 +03:00
Calculate all searches using oValidTokens i . e .
2016-09-04 04:19:48 +03:00
'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
*/
2017-10-12 23:37:44 +03:00
foreach ( $aPhrases as $iPhrase => $oPhrase ) {
2016-09-04 04:19:48 +03:00
$aNewPhraseSearches = array ();
2017-10-12 23:37:44 +03:00
$sPhraseType = $bIsStructured ? $oPhrase -> getPhraseType () : '' ;
2016-09-04 04:19:48 +03:00
2017-10-12 23:37:44 +03:00
foreach ( $oPhrase -> getWordSets () as $iWordSet => $aWordset ) {
2016-09-04 04:19:48 +03:00
// Too many permutations - too expensive
if ( $iWordSet > 120 ) break ;
$aWordsetSearches = $aSearches ;
// Add all words from this wordset
2016-09-08 04:16:22 +03:00
foreach ( $aWordset as $iToken => $sToken ) {
2016-09-04 04:19:48 +03:00
//echo "<br><b>$sToken</b>";
$aNewWordsetSearches = array ();
2017-10-08 13:57:22 +03:00
foreach ( $aWordsetSearches as $oCurrentSearch ) {
2016-09-04 04:19:48 +03:00
//echo "<i>";
2017-10-08 13:57:22 +03:00
//var_dump($oCurrentSearch);
2016-09-04 04:19:48 +03:00
//echo "</i>";
2018-05-15 00:04:15 +03:00
// Tokens with full name matches.
foreach ( $oValidTokens -> get ( ' ' . $sToken ) as $oSearchTerm ) {
$aNewSearches = $oCurrentSearch -> extendWithFullTerm (
$oSearchTerm ,
$oValidTokens -> contains ( $sToken )
&& strpos ( $sToken , ' ' ) === false ,
$sPhraseType ,
$iToken == 0 && $iPhrase == 0 ,
$iPhrase == 0 ,
$iToken + 1 == count ( $aWordset )
&& $iPhrase + 1 == count ( $aPhrases )
);
foreach ( $aNewSearches as $oSearch ) {
if ( $oSearch -> getRank () < $this -> iMaxRank ) {
$aNewWordsetSearches [] = $oSearch ;
2016-09-04 04:19:48 +03:00
}
}
}
// Look for partial matches.
// Note that there is no point in adding country terms here
2017-10-08 13:57:22 +03:00
// because country is omitted in the address.
2018-05-15 00:04:15 +03:00
if ( $sPhraseType != 'country' ) {
2016-09-04 04:19:48 +03:00
// Allow searching for a word - but at extra cost
2018-05-15 00:04:15 +03:00
foreach ( $oValidTokens -> get ( $sToken ) as $oSearchTerm ) {
2017-10-08 13:57:22 +03:00
$aNewSearches = $oCurrentSearch -> extendWithPartialTerm (
2018-05-15 00:04:15 +03:00
$sToken ,
$oSearchTerm ,
2017-10-12 23:37:44 +03:00
$bIsStructured ,
2017-10-08 13:57:22 +03:00
$iPhrase ,
2018-05-15 00:04:15 +03:00
$oValidTokens -> get ( ' ' . $sToken )
2017-10-08 13:57:22 +03:00
);
foreach ( $aNewSearches as $oSearch ) {
if ( $oSearch -> getRank () < $this -> iMaxRank ) {
$aNewWordsetSearches [] = $oSearch ;
2016-09-04 04:19:48 +03:00
}
}
}
}
}
// Sort and cut
2017-10-08 16:26:14 +03:00
usort ( $aNewWordsetSearches , array ( 'Nominatim\SearchDescription' , 'bySearchRank' ));
2016-09-04 04:19:48 +03:00
$aWordsetSearches = array_slice ( $aNewWordsetSearches , 0 , 50 );
}
2018-03-22 14:36:24 +03:00
//var_Dump('<hr>',count($aWordsetSearches)); exit;
2016-09-04 04:19:48 +03:00
$aNewPhraseSearches = array_merge ( $aNewPhraseSearches , $aNewWordsetSearches );
2017-10-08 16:26:14 +03:00
usort ( $aNewPhraseSearches , array ( 'Nominatim\SearchDescription' , 'bySearchRank' ));
2016-09-04 04:19:48 +03:00
$aSearchHash = array ();
2016-09-08 04:16:22 +03:00
foreach ( $aNewPhraseSearches as $iSearch => $aSearch ) {
2016-09-04 04:19:48 +03:00
$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 ();
2016-09-08 04:16:22 +03:00
foreach ( $aNewPhraseSearches as $aSearch ) {
2017-10-08 13:57:22 +03:00
$iRank = $aSearch -> getRank ();
if ( $iRank < $this -> iMaxRank ) {
if ( ! isset ( $aGroupedSearches [ $iRank ])) {
$aGroupedSearches [ $iRank ] = array ();
}
$aGroupedSearches [ $iRank ][] = $aSearch ;
2016-09-04 04:19:48 +03:00
}
}
ksort ( $aGroupedSearches );
$iSearchCount = 0 ;
$aSearches = array ();
2016-09-08 04:16:22 +03:00
foreach ( $aGroupedSearches as $iScore => $aNewSearches ) {
2018-03-22 14:36:24 +03:00
$iSearchCount += count ( $aNewSearches );
2016-09-04 04:19:48 +03:00
$aSearches = array_merge ( $aSearches , $aNewSearches );
if ( $iSearchCount > 50 ) break ;
}
}
2017-10-02 22:57:44 +03:00
2017-10-07 13:23:46 +03:00
// Revisit searches, drop bad searches and give penalty to unlikely combinations.
2017-10-02 22:57:44 +03:00
$aGroupedSearches = array ();
2017-10-08 13:57:22 +03:00
foreach ( $aSearches as $oSearch ) {
2017-10-13 23:23:39 +03:00
if ( ! $oSearch -> isValidSearch ()) {
2017-10-07 13:23:46 +03:00
continue ;
}
2017-10-25 00:17:47 +03:00
$iRank = $oSearch -> getRank ();
2017-10-08 16:26:14 +03:00
if ( ! isset ( $aGroupedSearches [ $iRank ])) {
2017-10-08 13:57:22 +03:00
$aGroupedSearches [ $iRank ] = array ();
}
$aGroupedSearches [ $iRank ][] = $oSearch ;
2017-10-02 22:57:44 +03:00
}
ksort ( $aGroupedSearches );
2016-09-04 04:19:48 +03:00
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 )
2018-01-12 01:05:28 +03:00
admin_level : see https :// wiki . openstreetmap . org / wiki / Admin_level
2016-09-04 04:19:48 +03:00
rank_search : rank in search hierarchy
2018-01-12 01:05:28 +03:00
( see also https :// wiki . openstreetmap . org / wiki / Nominatim / Development_overview #Country_to_street_level)
2016-09-04 04:19:48 +03:00
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
*/
2016-09-14 04:16:46 +03:00
2016-09-16 03:27:36 +03:00
public function lookup ()
2016-09-04 04:19:48 +03:00
{
2018-03-24 19:44:13 +03:00
Debug :: newFunction ( 'Geocode::lookup' );
2017-03-23 01:02:19 +03:00
if ( ! $this -> sQuery && ! $this -> aStructuredQuery ) return array ();
2016-09-04 04:19:48 +03:00
2018-03-24 19:44:13 +03:00
Debug :: printDebugArray ( 'Geocode' , $this );
2017-10-08 22:23:31 +03:00
$oCtx = new SearchContext ();
2017-10-08 23:44:01 +03:00
if ( $this -> aRoutePoints ) {
$oCtx -> setViewboxFromRoute (
$this -> oDB ,
$this -> aRoutePoints ,
$this -> aRouteWidth ,
$this -> bBoundedSearch
);
2017-10-10 00:12:13 +03:00
} elseif ( $this -> aViewBox ) {
2017-10-08 23:44:01 +03:00
$oCtx -> setViewboxFromBox ( $this -> aViewBox , $this -> bBoundedSearch );
}
2017-10-09 00:15:06 +03:00
if ( $this -> aExcludePlaceIDs ) {
$oCtx -> setExcludeList ( $this -> aExcludePlaceIDs );
}
2017-10-09 00:33:54 +03:00
if ( $this -> aCountryCodes ) {
$oCtx -> setCountryList ( $this -> aCountryCodes );
}
2017-10-08 23:44:01 +03:00
2018-03-24 19:44:13 +03:00
Debug :: newSection ( 'Query Preprocessing' );
2017-07-07 00:06:13 +03:00
$sNormQuery = $this -> normTerm ( $this -> sQuery );
2018-03-24 19:44:13 +03:00
Debug :: printVar ( 'Normalized query' , $sNormQuery );
2017-10-08 11:06:17 +03:00
$sLanguagePrefArraySQL = getArraySQL (
2017-10-26 22:21:21 +03:00
array_map ( 'getDBQuoted' , $this -> aLangPrefOrder )
2017-10-08 11:06:17 +03:00
);
2016-09-04 04:19:48 +03:00
$sQuery = $this -> sQuery ;
2016-10-12 23:25:04 +03:00
if ( ! preg_match ( '//u' , $sQuery )) {
2017-10-26 22:21:21 +03:00
userError ( 'Query string is not UTF-8 encoded.' );
2016-10-12 23:25:04 +03:00
}
2016-09-04 04:19:48 +03:00
// Conflicts between US state abreviations and various words for 'the' in different languages
2016-09-08 04:16:22 +03:00
if ( isset ( $this -> aLangPrefOrder [ 'name:en' ])) {
2018-02-07 02:48:18 +03:00
$sQuery = preg_replace ( '/(^|,)\s*il\s*(,|$)/i' , '\1illinois\2' , $sQuery );
$sQuery = preg_replace ( '/(^|,)\s*al\s*(,|$)/i' , '\1alabama\2' , $sQuery );
$sQuery = preg_replace ( '/(^|,)\s*la\s*(,|$)/i' , '\1louisiana\2' , $sQuery );
2016-09-04 04:19:48 +03:00
}
// Do we have anything that looks like a lat/lon pair?
2017-10-08 22:23:31 +03:00
$sQuery = $oCtx -> setNearPointFromQuery ( $sQuery );
2016-09-04 04:19:48 +03:00
2017-10-22 22:59:08 +03:00
$aResults = array ();
2016-09-08 04:16:22 +03:00
if ( $sQuery || $this -> aStructuredQuery ) {
2017-10-06 01:14:48 +03:00
// Start with a single blank search
2017-10-08 22:23:31 +03:00
$aSearches = array ( new SearchDescription ( $oCtx ));
2016-09-04 04:19:48 +03:00
2017-10-06 01:14:48 +03:00
if ( $sQuery ) {
$sQuery = $aSearches [ 0 ] -> extractKeyValuePairs ( $sQuery );
2016-09-04 04:19:48 +03:00
}
2016-09-08 04:16:22 +03:00
2017-10-06 01:14:48 +03:00
$sSpecialTerm = '' ;
if ( $sQuery ) {
preg_match_all (
'/\\[([\\w ]*)\\]/u' ,
$sQuery ,
$aSpecialTermsRaw ,
PREG_SET_ORDER
);
2018-03-24 19:44:13 +03:00
if ( ! empty ( $aSpecialTermsRaw )) {
Debug :: printVar ( 'Special terms' , $aSpecialTermsRaw );
}
2017-10-06 01:14:48 +03:00
foreach ( $aSpecialTermsRaw as $aSpecialTerm ) {
$sQuery = str_replace ( $aSpecialTerm [ 0 ], ' ' , $sQuery );
if ( ! $sSpecialTerm ) {
$sSpecialTerm = $aSpecialTerm [ 1 ];
}
2017-09-18 23:07:08 +03:00
}
2017-10-06 01:14:48 +03:00
}
if ( ! $sSpecialTerm && $this -> aStructuredQuery
&& isset ( $this -> aStructuredQuery [ 'amenity' ])) {
$sSpecialTerm = $this -> aStructuredQuery [ 'amenity' ];
unset ( $this -> aStructuredQuery [ 'amenity' ]);
}
2017-09-18 23:07:08 +03:00
2017-10-06 01:14:48 +03:00
if ( $sSpecialTerm && ! $aSearches [ 0 ] -> hasOperator ()) {
$sSpecialTerm = pg_escape_string ( $sSpecialTerm );
$sToken = chksql (
$this -> oDB -> getOne ( " SELECT make_standard_name(' $sSpecialTerm ') " ),
2017-10-26 22:21:21 +03:00
'Cannot decode query. Wrong encoding?'
2017-10-06 01:14:48 +03:00
);
$sSQL = 'SELECT class, type FROM word ' ;
2016-10-29 19:49:21 +03:00
$sSQL .= ' WHERE word_token in (\' ' . $sToken . '\')' ;
2017-10-06 01:14:48 +03:00
$sSQL .= ' AND class is not null AND class not in (\'place\')' ;
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2016-09-04 04:19:48 +03:00
$aSearchWords = chksql ( $this -> oDB -> getAll ( $sSQL ));
$aNewSearches = array ();
2017-10-06 01:14:48 +03:00
foreach ( $aSearches as $oSearch ) {
2016-09-08 04:16:22 +03:00
foreach ( $aSearchWords as $aSearchTerm ) {
2017-10-06 01:14:48 +03:00
$oNewSearch = clone $oSearch ;
$oNewSearch -> setPoiSearch (
Operator :: TYPE ,
$aSearchTerm [ 'class' ],
2017-10-08 16:26:14 +03:00
$aSearchTerm [ 'type' ]
2017-10-06 01:14:48 +03:00
);
$aNewSearches [] = $oNewSearch ;
2016-09-04 04:19:48 +03:00
}
}
$aSearches = $aNewSearches ;
}
// Split query into phrases
// Commas are used to reduce the search space by indicating where phrases split
2016-09-08 04:16:22 +03:00
if ( $this -> aStructuredQuery ) {
2017-10-12 23:37:44 +03:00
$aInPhrases = $this -> aStructuredQuery ;
2016-09-04 04:19:48 +03:00
$bStructuredPhrases = true ;
2016-09-08 04:16:22 +03:00
} else {
2017-10-12 23:37:44 +03:00
$aInPhrases = explode ( ',' , $sQuery );
2016-09-04 04:19:48 +03:00
$bStructuredPhrases = false ;
}
2018-03-24 19:44:13 +03:00
Debug :: printDebugArray ( 'Search context' , $oCtx );
2018-03-27 04:00:07 +03:00
Debug :: printDebugArray ( 'Base search' , empty ( $aSearches ) ? null : $aSearches [ 0 ]);
2018-03-24 19:44:13 +03:00
Debug :: printVar ( 'Final query phrases' , $aInPhrases );
2016-09-04 04:19:48 +03:00
// Convert each phrase to standard form
// Create a list of standard words
// Get all 'sets' of words
// Generate a complete list of all
2018-03-24 19:44:13 +03:00
Debug :: newSection ( 'Tokenization' );
2016-09-04 04:19:48 +03:00
$aTokens = array ();
2017-10-12 23:37:44 +03:00
$aPhrases = array ();
foreach ( $aInPhrases as $iPhrase => $sPhrase ) {
$sPhrase = chksql (
$this -> oDB -> getOne ( 'SELECT make_standard_name(' . getDBQuoted ( $sPhrase ) . ')' ),
2017-10-26 22:21:21 +03:00
'Cannot normalize query string (is it a UTF-8 string?)'
2016-09-11 06:22:51 +03:00
);
2017-10-12 23:37:44 +03:00
if ( trim ( $sPhrase )) {
$oPhrase = new Phrase ( $sPhrase , is_string ( $iPhrase ) ? $iPhrase : '' );
$oPhrase -> addTokens ( $aTokens );
$aPhrases [] = $oPhrase ;
2016-09-04 04:19:48 +03:00
}
}
2018-03-24 19:44:13 +03:00
Debug :: printDebugTable ( 'Phrases' , $aPhrases );
Debug :: printVar ( 'Tokens' , $aTokens );
2018-05-15 00:04:15 +03:00
$oValidTokens = new TokenList ();
2018-03-22 14:36:24 +03:00
if ( ! empty ( $aTokens )) {
2016-10-29 19:49:21 +03:00
$sSQL = 'SELECT word_id, word_token, word, class, type, country_code, operator, search_name_count' ;
$sSQL .= ' FROM word ' ;
2017-10-26 22:21:21 +03:00
$sSQL .= ' WHERE word_token in (' . join ( ',' , array_map ( 'getDBQuoted' , $aTokens )) . ')' ;
2016-09-04 04:19:48 +03:00
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2016-09-04 04:19:48 +03:00
2018-05-15 00:04:15 +03:00
$oValidTokens -> addTokensFromDB (
$this -> oDB ,
$aTokens ,
$this -> aCountryCodes ,
$sNormQuery ,
$this -> oNormalizer
2017-10-02 23:31:52 +03:00
);
2017-10-14 00:04:12 +03:00
2018-05-15 00:04:15 +03:00
// Try more interpretations for Tokens that could not be matched.
2016-09-08 04:16:22 +03:00
foreach ( $aTokens as $sToken ) {
2018-05-15 00:04:15 +03:00
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 ))
);
2016-09-04 04:19:48 +03:00
}
}
}
// Any words that have failed completely?
// TODO: suggestions
2018-05-15 00:04:15 +03:00
Debug :: printGroupTable ( 'Valid Tokens' , $oValidTokens -> debugInfo ());
2018-03-24 19:44:13 +03:00
Debug :: newSection ( 'Search candidates' );
2016-09-04 04:19:48 +03:00
2018-05-15 00:04:15 +03:00
$aGroupedSearches = $this -> getGroupedSearches ( $aSearches , $aPhrases , $oValidTokens , $bStructuredPhrases );
2016-09-04 04:19:48 +03:00
2016-09-08 04:16:22 +03:00
if ( $this -> bReverseInPlan ) {
2016-09-04 04:19:48 +03:00
// 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 );
2017-10-12 23:37:44 +03:00
$aPhrases [ 0 ] -> invertWordSets ();
2018-03-22 14:36:24 +03:00
if ( count ( $aPhrases ) > 1 ) {
$aPhrases [ count ( $aPhrases ) - 1 ] -> invertWordSets ();
2016-09-04 04:19:48 +03:00
}
2018-05-15 00:04:15 +03:00
$aReverseGroupedSearches = $this -> getGroupedSearches ( $aSearches , $aPhrases , $oValidTokens , false );
2016-09-04 04:19:48 +03:00
2016-09-08 04:16:22 +03:00
foreach ( $aGroupedSearches as $aSearches ) {
foreach ( $aSearches as $aSearch ) {
2017-10-06 01:14:48 +03:00
if ( ! isset ( $aReverseGroupedSearches [ $aSearch -> getRank ()])) {
$aReverseGroupedSearches [ $aSearch -> getRank ()] = array ();
2016-09-04 04:19:48 +03:00
}
2017-10-06 01:14:48 +03:00
$aReverseGroupedSearches [ $aSearch -> getRank ()][] = $aSearch ;
2016-09-04 04:19:48 +03:00
}
}
$aGroupedSearches = $aReverseGroupedSearches ;
ksort ( $aGroupedSearches );
}
2016-09-08 04:16:22 +03:00
} else {
2016-09-04 04:19:48 +03:00
// Re-group the searches by their score, junk anything over 20 as just not worth trying
$aGroupedSearches = array ();
2016-09-08 04:16:22 +03:00
foreach ( $aSearches as $aSearch ) {
2017-10-06 01:14:48 +03:00
if ( $aSearch -> getRank () < $this -> iMaxRank ) {
if ( ! isset ( $aGroupedSearches [ $aSearch -> getRank ()])) $aGroupedSearches [ $aSearch -> getRank ()] = array ();
$aGroupedSearches [ $aSearch -> getRank ()][] = $aSearch ;
2016-09-04 04:19:48 +03:00
}
}
ksort ( $aGroupedSearches );
}
// Filter out duplicate searches
$aSearchHash = array ();
2016-09-08 04:16:22 +03:00
foreach ( $aGroupedSearches as $iGroup => $aSearches ) {
foreach ( $aSearches as $iSearch => $aSearch ) {
2016-09-04 04:19:48 +03:00
$sHash = serialize ( $aSearch );
2016-09-08 04:16:22 +03:00
if ( isset ( $aSearchHash [ $sHash ])) {
2016-09-04 04:19:48 +03:00
unset ( $aGroupedSearches [ $iGroup ][ $iSearch ]);
2018-03-22 14:36:24 +03:00
if ( empty ( $aGroupedSearches [ $iGroup ])) unset ( $aGroupedSearches [ $iGroup ]);
2016-09-08 04:16:22 +03:00
} else {
2016-09-04 04:19:48 +03:00
$aSearchHash [ $sHash ] = 1 ;
}
}
}
2018-05-15 00:04:15 +03:00
Debug :: printGroupedSearch (
$aGroupedSearches ,
$oValidTokens -> debugTokenByWordIdList ()
);
2016-09-04 04:19:48 +03:00
2017-10-09 23:55:50 +03:00
// Start the search process
2016-09-04 04:19:48 +03:00
$iGroupLoop = 0 ;
$iQueryLoop = 0 ;
2016-09-08 04:16:22 +03:00
foreach ( $aGroupedSearches as $iGroupedRank => $aSearches ) {
2016-09-04 04:19:48 +03:00
$iGroupLoop ++ ;
2017-10-07 13:01:56 +03:00
foreach ( $aSearches as $oSearch ) {
2016-09-04 04:19:48 +03:00
$iQueryLoop ++ ;
2018-05-15 00:04:15 +03:00
Debug :: newSection ( " Search Loop, group $iGroupLoop , loop $iQueryLoop " );
Debug :: printGroupedSearch (
array ( $iGroupedRank => array ( $oSearch )),
$oValidTokens -> debugTokenByWordIdList ()
);
2016-09-04 04:19:48 +03:00
2017-10-19 00:54:47 +03:00
$aResults += $oSearch -> query (
2017-10-09 23:55:50 +03:00
$this -> oDB ,
$this -> iMinAddressRank ,
$this -> iMaxAddressRank ,
$this -> iLimit
);
2017-07-06 00:35:22 +03:00
2016-09-04 04:19:48 +03:00
if ( $iQueryLoop > 20 ) break ;
}
2018-03-22 14:36:24 +03:00
if ( ! empty ( $aResults ) && ( $this -> iMinAddressRank != 0 || $this -> iMaxAddressRank != 30 )) {
2016-09-04 04:19:48 +03:00
// 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
2017-10-19 00:54:47 +03:00
$aFilterSql = array ();
$sPlaceIds = Result :: joinIdsByTable ( $aResults , Result :: TABLE_PLACEX );
if ( $sPlaceIds ) {
$sSQL = 'SELECT place_id FROM placex ' ;
$sSQL .= 'WHERE place_id in (' . $sPlaceIds . ') ' ;
2017-10-26 22:21:21 +03:00
$sSQL .= ' AND (' ;
2017-10-19 00:54:47 +03:00
$sSQL .= " placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank " ;
if ( 14 >= $this -> iMinAddressRank && 14 <= $this -> iMaxAddressRank ) {
$sSQL .= " OR (extratags->'place') = 'city' " ;
}
if ( $this -> aAddressRankList ) {
2017-10-26 22:21:21 +03:00
$sSQL .= ' OR placex.rank_address in (' . join ( ',' , $this -> aAddressRankList ) . ')' ;
2017-10-19 00:54:47 +03:00
}
2017-10-26 22:21:21 +03:00
$sSQL .= ')' ;
2017-10-19 00:54:47 +03:00
$aFilterSql [] = $sSQL ;
2017-10-03 13:10:27 +03:00
}
2017-10-19 00:54:47 +03:00
$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 ) {
2017-10-26 22:21:21 +03:00
$sSQL .= ' OR lp.rank_address in (' . join ( ',' , $this -> aAddressRankList ) . ')' ;
2017-10-19 00:54:47 +03:00
}
2017-10-26 22:21:21 +03:00
$sSQL .= ') ' ;
2017-10-19 00:54:47 +03:00
$aFilterSql [] = $sSQL ;
2016-09-04 04:19:48 +03:00
}
2017-10-19 00:54:47 +03:00
$aFilteredIDs = array ();
if ( $aFilterSql ) {
$sSQL = join ( ' UNION ' , $aFilterSql );
2018-03-24 19:44:13 +03:00
Debug :: printSQL ( $sSQL );
2017-10-19 00:54:47 +03:00
$aFilteredIDs = chksql ( $this -> oDB -> getCol ( $sSQL ));
2017-10-02 21:42:37 +03:00
}
2017-10-19 00:54:47 +03:00
2016-09-04 04:19:48 +03:00
$tempIDs = array ();
2017-10-19 00:54:47 +03:00
foreach ( $aResults as $oResult ) {
if (( $this -> iMaxAddressRank == 30 &&
( $oResult -> iTable == Result :: TABLE_OSMLINE
|| $oResult -> iTable == Result :: TABLE_AUX
|| $oResult -> iTable == Result :: TABLE_TIGER ))
|| in_array ( $oResult -> iId , $aFilteredIDs )
) {
$tempIDs [ $oResult -> iId ] = $oResult ;
}
2016-09-04 04:19:48 +03:00
}
2017-10-19 00:54:47 +03:00
$aResults = $tempIDs ;
2016-09-04 04:19:48 +03:00
}
2018-03-22 14:36:24 +03:00
if ( ! empty ( $aResults )) break ;
2016-09-04 04:19:48 +03:00
if ( $iGroupLoop > 4 ) break ;
if ( $iQueryLoop > 30 ) break ;
}
2016-09-08 04:16:22 +03:00
} else {
2016-09-04 04:19:48 +03:00
// Just interpret as a reverse geocode
2016-09-30 23:42:12 +03:00
$oReverse = new ReverseGeocode ( $this -> oDB );
2016-09-04 04:19:48 +03:00
$oReverse -> setZoom ( 18 );
2017-10-20 16:41:26 +03:00
$oLookup = $oReverse -> lookupPoint ( $oCtx -> sqlNear , false );
2016-09-04 04:19:48 +03:00
2018-03-24 19:44:13 +03:00
Debug :: printVar ( 'Reverse search' , $oLookup );
2016-09-04 04:19:48 +03:00
2017-10-20 16:41:26 +03:00
if ( $oLookup ) {
$aResults = array ( $oLookup -> iId => $oLookup );
2016-09-08 04:16:22 +03:00
}
2016-09-04 04:19:48 +03:00
}
// No results? Done
2018-03-22 14:36:24 +03:00
if ( empty ( $aResults )) {
2016-09-08 04:16:22 +03:00
if ( $this -> bFallback ) {
if ( $this -> fallbackStructuredQuery ()) {
2016-09-04 04:19:48 +03:00
return $this -> lookup ();
}
}
return array ();
}
2017-10-22 22:59:08 +03:00
if ( $this -> aAddressRankList ) {
2017-10-24 00:28:00 +03:00
$this -> oPlaceLookup -> setAddressRankList ( $this -> aAddressRankList );
2017-10-22 22:59:08 +03:00
}
2017-10-24 00:28:00 +03:00
$this -> oPlaceLookup -> setAllowedTypesSQLList ( $this -> sAllowedTypesSQLList );
$this -> oPlaceLookup -> setLanguagePreference ( $this -> aLangPrefOrder );
2017-10-22 22:59:08 +03:00
if ( $oCtx -> hasNearPoint ()) {
2017-10-24 00:28:00 +03:00
$this -> oPlaceLookup -> setAnchorSql ( $oCtx -> sqlNear );
2017-10-22 22:59:08 +03:00
}
2017-10-24 00:28:00 +03:00
$aSearchResults = $this -> oPlaceLookup -> lookup ( $aResults );
2017-10-22 22:59:08 +03:00
2016-09-04 04:19:48 +03:00
$aClassType = getClassTypesWithImportance ();
2016-09-11 06:22:51 +03:00
$aRecheckWords = preg_split ( '/\b[\s,\\-]*/u' , $sQuery );
2016-09-08 04:16:22 +03:00
foreach ( $aRecheckWords as $i => $sWord ) {
2017-02-28 23:40:05 +03:00
if ( ! preg_match ( '/[\pL\pN]/' , $sWord )) unset ( $aRecheckWords [ $i ]);
2016-09-04 04:19:48 +03:00
}
2018-03-24 19:44:13 +03:00
Debug :: printVar ( 'Recheck words' , $aRecheckWords );
2016-09-04 04:19:48 +03:00
2017-10-22 22:59:08 +03:00
foreach ( $aSearchResults as $iIdx => $aResult ) {
2016-09-04 04:19:48 +03:00
// Default
$fDiameter = getResultDiameter ( $aResult );
2017-10-24 00:28:00 +03:00
$aOutlineResult = $this -> oPlaceLookup -> getOutlines ( $aResult [ 'place_id' ], $aResult [ 'lon' ], $aResult [ 'lat' ], $fDiameter / 2 );
2016-09-08 04:16:22 +03:00
if ( $aOutlineResult ) {
2016-09-04 04:19:48 +03:00
$aResult = array_merge ( $aResult , $aOutlineResult );
}
2017-10-22 22:59:08 +03:00
2016-09-08 04:16:22 +03:00
if ( $aResult [ 'extra_place' ] == 'city' ) {
2016-09-04 04:19:48 +03:00
$aResult [ 'class' ] = 'place' ;
$aResult [ 'type' ] = 'city' ;
$aResult [ 'rank_search' ] = 16 ;
}
// Is there an icon set for this type of result?
if ( isset ( $aClassType [ $aResult [ 'class' ] . ':' . $aResult [ 'type' ]][ 'icon' ])
2016-09-11 06:22:51 +03:00
&& $aClassType [ $aResult [ 'class' ] . ':' . $aResult [ 'type' ]][ 'icon' ]
2016-09-08 04:16:22 +03:00
) {
2016-09-04 04:19:48 +03:00
$aResult [ 'icon' ] = CONST_Website_BaseURL . 'images/mapicons/' . $aClassType [ $aResult [ 'class' ] . ':' . $aResult [ 'type' ]][ 'icon' ] . '.p.20.png' ;
}
if ( isset ( $aClassType [ $aResult [ 'class' ] . ':' . $aResult [ 'type' ] . ':' . $aResult [ 'admin_level' ]][ 'label' ])
2016-09-11 06:22:51 +03:00
&& $aClassType [ $aResult [ 'class' ] . ':' . $aResult [ 'type' ] . ':' . $aResult [ 'admin_level' ]][ 'label' ]
2016-09-08 04:16:22 +03:00
) {
2016-09-04 04:19:48 +03:00
$aResult [ 'label' ] = $aClassType [ $aResult [ 'class' ] . ':' . $aResult [ 'type' ] . ':' . $aResult [ 'admin_level' ]][ 'label' ];
2016-09-08 04:16:22 +03:00
} elseif ( isset ( $aClassType [ $aResult [ 'class' ] . ':' . $aResult [ 'type' ]][ 'label' ])
2016-09-11 06:22:51 +03:00
&& $aClassType [ $aResult [ 'class' ] . ':' . $aResult [ 'type' ]][ 'label' ]
2016-09-08 04:16:22 +03:00
) {
2016-09-04 04:19:48 +03:00
$aResult [ 'label' ] = $aClassType [ $aResult [ 'class' ] . ':' . $aResult [ 'type' ]][ 'label' ];
}
// if tag '&addressdetails=1' is set in query
2016-09-08 04:16:22 +03:00
if ( $this -> bIncludeAddressDetails ) {
2016-09-04 04:19:48 +03:00
// getAddressDetails() is defined in lib.php and uses the SQL function get_addressdata in functions.sql
2017-10-19 00:54:47 +03:00
$aResult [ 'address' ] = getAddressDetails ( $this -> oDB , $sLanguagePrefArraySQL , $aResult [ 'place_id' ], $aResult [ 'country_code' ], $aResults [ $aResult [ 'place_id' ]] -> iHouseNumber );
2016-09-08 04:16:22 +03:00
if ( $aResult [ 'extra_place' ] == 'city' && ! isset ( $aResult [ 'address' ][ 'city' ])) {
2016-10-09 22:14:59 +03:00
$aResult [ 'address' ] = array_merge ( array ( 'city' => array_values ( $aResult [ 'address' ])[ 0 ]), $aResult [ 'address' ]);
2016-09-04 04:19:48 +03:00
}
}
2016-09-08 04:16:22 +03:00
2017-10-10 23:42:23 +03:00
$aResult [ 'name' ] = $aResult [ 'langaddress' ];
2017-10-14 00:11:09 +03:00
if ( $oCtx -> hasNearPoint ()) {
2017-10-10 23:42:23 +03:00
$aResult [ 'importance' ] = 0.001 ;
$aResult [ 'foundorder' ] = $aResult [ 'addressimportance' ];
} else {
2018-02-20 01:24:12 +03:00
$aResult [ 'importance' ] = max ( 0.001 , $aResult [ 'importance' ]);
2017-10-22 22:59:08 +03:00
$aResult [ 'importance' ] *= $this -> viewboxImportanceFactor (
$aResult [ 'lon' ],
$aResult [ 'lat' ]
);
2018-02-20 01:24:12 +03:00
// Adjust importance for the number of exact string matches in the result
2017-10-10 23:42:23 +03:00
$iCountWords = 0 ;
$sAddress = $aResult [ 'langaddress' ];
foreach ( $aRecheckWords as $i => $sWord ) {
if ( stripos ( $sAddress , $sWord ) !== false ) {
$iCountWords ++ ;
2017-10-26 22:21:21 +03:00
if ( preg_match ( '/(^|,)\s*' . preg_quote ( $sWord , '/' ) . '\s*(,|$)/' , $sAddress )) $iCountWords += 0.1 ;
2017-10-10 23:42:23 +03:00
}
2016-09-04 04:19:48 +03:00
}
2017-10-10 23:42:23 +03:00
$aResult [ 'importance' ] = $aResult [ 'importance' ] + ( $iCountWords * 0.1 ); // 0.1 is a completely arbitrary number but something in the range 0.1 to 0.5 would seem right
2016-09-04 04:19:48 +03:00
2017-10-10 23:42:23 +03:00
// secondary ordering (for results with same importance (the smaller the better):
// - approximate importance of address parts
$aResult [ 'foundorder' ] = - $aResult [ 'addressimportance' ] / 10 ;
// - number of exact matches from the query
2017-10-19 00:54:47 +03:00
$aResult [ 'foundorder' ] -= $aResults [ $aResult [ 'place_id' ]] -> iExactMatches ;
2017-10-10 23:42:23 +03:00
// - importance of the class/type
if ( isset ( $aClassType [ $aResult [ 'class' ] . ':' . $aResult [ 'type' ]][ 'importance' ])
&& $aClassType [ $aResult [ 'class' ] . ':' . $aResult [ 'type' ]][ 'importance' ]
) {
$aResult [ 'foundorder' ] += 0.0001 * $aClassType [ $aResult [ 'class' ] . ':' . $aResult [ 'type' ]][ 'importance' ];
} else {
$aResult [ 'foundorder' ] += 0.01 ;
}
2016-09-04 04:19:48 +03:00
}
2017-10-22 22:59:08 +03:00
$aSearchResults [ $iIdx ] = $aResult ;
2016-09-04 04:19:48 +03:00
}
uasort ( $aSearchResults , 'byImportance' );
2018-03-24 19:44:13 +03:00
Debug :: printVar ( 'Pre-filter results' , $aSearchResults );
2016-09-04 04:19:48 +03:00
$aOSMIDDone = array ();
$aClassTypeNameDone = array ();
$aToFilter = $aSearchResults ;
$aSearchResults = array ();
$bFirst = true ;
2017-10-22 22:59:08 +03:00
foreach ( $aToFilter as $aResult ) {
2016-09-04 04:19:48 +03:00
$this -> aExcludePlaceIDs [ $aResult [ 'place_id' ]] = $aResult [ 'place_id' ];
2016-09-08 04:16:22 +03:00
if ( $bFirst ) {
2016-09-04 04:19:48 +03:00
$fLat = $aResult [ 'lat' ];
$fLon = $aResult [ 'lon' ];
if ( isset ( $aResult [ 'zoom' ])) $iZoom = $aResult [ 'zoom' ];
$bFirst = false ;
}
2017-10-24 00:28:00 +03:00
if ( ! $this -> oPlaceLookup -> doDeDupe () || ( ! isset ( $aOSMIDDone [ $aResult [ 'osm_type' ] . $aResult [ 'osm_id' ]])
2016-09-11 06:22:51 +03:00
&& ! isset ( $aClassTypeNameDone [ $aResult [ 'osm_type' ] . $aResult [ 'class' ] . $aResult [ 'type' ] . $aResult [ 'name' ] . $aResult [ 'admin_level' ]]))
2016-09-08 04:16:22 +03:00
) {
2016-09-04 04:19:48 +03:00
$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
2018-03-22 14:36:24 +03:00
if ( count ( $aSearchResults ) >= $this -> iFinalLimit ) break ;
2016-09-04 04:19:48 +03:00
}
2018-03-24 19:44:13 +03:00
Debug :: printVar ( 'Post-filter results' , $aSearchResults );
2016-09-04 04:19:48 +03:00
return $aSearchResults ;
} // end lookup()
2018-03-24 19:44:13 +03:00
public function debugInfo ()
{
return array (
'Query' => $this -> sQuery ,
'Structured query' => $this -> aStructuredQuery ,
'Name keys' => Debug :: fmtArrayVals ( $this -> aLangPrefOrder ),
'Include address' => $this -> bIncludeAddressDetails ,
'Excluded place IDs' => Debug :: fmtArrayVals ( $this -> aExcludePlaceIDs ),
'Try reversed query' => $this -> bReverseInPlan ,
'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 )
);
}
2016-09-04 04:19:48 +03:00
} // end class