6  * Operators describing special searches.
 
   8 abstract class Operator
 
  10     /// No operator selected.
 
  12     /// Search for POI of the given type.
 
  14     /// Search for POIs near the given place.
 
  16     /// Search for POIS in the given place.
 
  18     /// Search for POIS named as given.
 
  20     /// Search for postcodes.
 
  25  * Description of a single interpretation of a search query.
 
  27 class SearchDescription
 
  29     /// Ranking how well the description fits the query.
 
  30     private $iSearchRank = 0;
 
  31     /// Country code of country the result must belong to.
 
  32     private $sCountryCode = '';
 
  33     /// List of word ids making up the name of the object.
 
  34     private $aName = array();
 
  35     /// List of word ids making up the address of the object.
 
  36     private $aAddress = array();
 
  37     /// Subset of word ids of full words making up the address.
 
  38     private $aFullNameAddress = array();
 
  39     /// List of word ids that appear in the name but should be ignored.
 
  40     private $aNameNonSearch = array();
 
  41     /// List of word ids that appear in the address but should be ignored.
 
  42     private $aAddressNonSearch = array();
 
  43     /// Kind of search for special searches, see Nominatim::Operator.
 
  44     private $iOperator = Operator::NONE;
 
  45     /// Class of special feature to search for.
 
  47     /// Type of special feature to search for.
 
  49     /// Housenumber of the object.
 
  50     private $sHouseNumber = '';
 
  51     /// Postcode for the object.
 
  52     private $sPostcode = '';
 
  53     /// Geographic search area.
 
  54     private $oNearPoint = false;
 
  56     // Temporary values used while creating the search description.
 
  58     /// Index of phrase currently processed
 
  59     private $iNamePhrase = -1;
 
  61     public function getRank()
 
  63         return $this->iSearchRank;
 
  66     public function addToRank($iAddRank)
 
  68         $this->iSearchRank += $iAddRank;
 
  69         return $this->iSearchRank;
 
  72     public function getPostCode()
 
  74         return $this->sPostcode;
 
  78      * Set the geographic search radius.
 
  80     public function setNear(&$oNearPoint)
 
  82         $this->oNearPoint = $oNearPoint;
 
  85     public function setPoiSearch($iOperator, $sClass, $sType)
 
  87         $this->iOperator = $iOperator;
 
  88         $this->sClass = $sClass;
 
  89         $this->sType = $sType;
 
  93      * Check if name or address for the search are specified.
 
  95     public function isNamedSearch()
 
  97         return sizeof($this->aName) > 0 || sizeof($this->aAddress) > 0;
 
 101      * Check if only a country is requested.
 
 103     public function isCountrySearch()
 
 105         return $this->sCountryCode && sizeof($this->aName) == 0
 
 106                && !$this->iOperator && !$this->oNearPoint;
 
 110      * Check if a search near a geographic location is requested.
 
 112     public function isNearSearch()
 
 114         return (bool) $this->oNearPoint;
 
 117     public function isPoiSearch()
 
 119         return (bool) $this->sClass;
 
 122     public function looksLikeFullAddress()
 
 124         return sizeof($this->aName)
 
 125                && (sizeof($this->aAddress || $this->sCountryCode))
 
 126                && preg_match('/[0-9]+/', $this->sHouseNumber);
 
 129     public function isOperator($iType)
 
 131         return $this->iOperator == $iType;
 
 134     public function hasHouseNumber()
 
 136         return (bool) $this->sHouseNumber;
 
 139     private function poiTable()
 
 141         return 'place_classtype_'.$this->sClass.'_'.$this->sType;
 
 144     public function countryCodeSQL($sVar, $sCountryList)
 
 146         if ($this->sCountryCode) {
 
 147             return $sVar.' = \''.$this->sCountryCode."'";
 
 150             return $sVar.' in ('.$sCountryList.')';
 
 156     public function hasOperator()
 
 158         return $this->iOperator != Operator::NONE;
 
 162      * Extract special terms from the query, amend the search
 
 163      * and return the shortended query.
 
 165      * Only the first special term found will be used but all will
 
 166      * be removed from the query.
 
 168     public function extractKeyValuePairs($sQuery)
 
 170         // Search for terms of kind [<key>=<value>].
 
 172             '/\\[([\\w_]*)=([\\w_]*)\\]/',
 
 178         foreach ($aSpecialTermsRaw as $aTerm) {
 
 179             $sQuery = str_replace($aTerm[0], ' ', $sQuery);
 
 180             if (!$this->hasOperator()) {
 
 181                 $this->setPoiSearch(Operator::TYPE, $aTerm[1], $aTerm[2]);
 
 188     public function isValidSearch(&$aCountryCodes)
 
 190         if (!sizeof($this->aName)) {
 
 191             if ($this->sHouseNumber) {
 
 196             && $this->sCountryCode
 
 197             && !in_array($this->sCountryCode, $aCountryCodes)
 
 205     /////////// Search building functions
 
 207     public function extendWithFullTerm($aSearchTerm, $bWordInQuery, $bHasPartial, $sPhraseType, $bFirstToken, $bFirstPhrase, $bLastToken, &$iGlobalRank)
 
 209         $aNewSearches = array();
 
 211         if (($sPhraseType == '' || $sPhraseType == 'country')
 
 212             && !empty($aSearchTerm['country_code'])
 
 213             && $aSearchTerm['country_code'] != '0'
 
 215             if (!$this->sCountryCode) {
 
 216                 $oSearch = clone $this;
 
 217                 $oSearch->iSearchRank++;
 
 218                 $oSearch->sCountryCode = $aSearchTerm['country_code'];
 
 219                 // Country is almost always at the end of the string
 
 220                 // - increase score for finding it anywhere else (optimisation)
 
 222                     $oSearch->iSearchRank += 5;
 
 224                 $aNewSearches[] = $oSearch;
 
 226                 // If it is at the beginning, we can be almost sure that
 
 227                 // the terms are in the wrong order. Increase score for all searches.
 
 232         } elseif (($sPhraseType == '' || $sPhraseType == 'postalcode')
 
 233                   && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'postcode'
 
 235             // We need to try the case where the postal code is the primary element
 
 236             // (i.e. no way to tell if it is (postalcode, city) OR (city, postalcode)
 
 238             if (!$this->sPostcode && $bWordInQuery) {
 
 239                 // If we have structured search or this is the first term,
 
 240                 // make the postcode the primary search element.
 
 241                 if ($this->iOperator == Operator::NONE
 
 242                     && ($sPhraseType == 'postalcode' || $bFirstToken)
 
 244                     $oSearch = clone $this;
 
 245                     $oSearch->iSearchRank++;
 
 246                     $oSearch->iOperator = Operator::POSTCODE;
 
 247                     $oSearch->aAddress = array_merge($this->aAddress, $this->aName);
 
 249                         array($aSearchTerm['word_id'] => $aSearchTerm['word']);
 
 250                     $aNewSearches[] = $oSearch;
 
 253                 // If we have a structured search or this is not the first term,
 
 254                 // add the postcode as an addendum.
 
 255                 if ($this->iOperator != Operator::POSTCODE
 
 256                     && ($sPhraseType == 'postalcode' || sizeof($this->aName))
 
 258                     $oSearch = clone $this;
 
 259                     $oSearch->iSearchRank++;
 
 260                     $oSearch->sPostcode = $aSearchTerm['word'];
 
 261                     $aNewSearches[] = $oSearch;
 
 264         } elseif (($sPhraseType == '' || $sPhraseType == 'street')
 
 265                  && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house'
 
 267             if (!$this->sHouseNumber && $this->iOperator != Operator::POSTCODE) {
 
 268                 $oSearch = clone $this;
 
 269                 $oSearch->iSearchRank++;
 
 270                 $oSearch->sHouseNumber = trim($aSearchTerm['word_token']);
 
 271                 // sanity check: if the housenumber is not mainly made
 
 272                 // up of numbers, add a penalty
 
 273                 if (preg_match_all("/[^0-9]/", $oSearch->sHouseNumber, $aMatches) > 2) {
 
 274                     $oSearch->iSearchRank++;
 
 276                 // also must not appear in the middle of the address
 
 277                 if (sizeof($this->aAddress) || sizeof($this->aAddressNonSearch)) {
 
 278                     $oSearch->iSearchRank++;
 
 280                 $aNewSearches[] = $oSearch;
 
 282         } elseif ($sPhraseType == ''
 
 283                   && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null
 
 285             // require a normalized exact match of the term
 
 286             // if we have the normalizer version of the query
 
 288             if ($this->iOperator == Operator::NONE
 
 289                 && (isset($aSearchTerm['word']) && $aSearchTerm['word'])
 
 292                 $oSearch = clone $this;
 
 293                 $oSearch->iSearchRank++;
 
 295                 $iOp = Operator::NEAR; // near == in for the moment
 
 296                 if ($aSearchTerm['operator'] == '') {
 
 297                     if (sizeof($this->aName)) {
 
 298                         $iOp = Operator::NAME;
 
 300                     $oSearch->iSearchRank += 2;
 
 303                 $oSearch->setPoiSearch($iOp, $aSearchTerm['class'], $aSearchTerm['type']);
 
 304                 $aNewWordsetSearches[] = $oSearch;
 
 306         } elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
 
 307             $iWordID = $aSearchTerm['word_id'];
 
 308             if (sizeof($this->aName)) {
 
 309                 if (($sPhraseType == '' || !$bFirstPhrase)
 
 310                     && $sPhraseType != 'country'
 
 313                     $oSearch = clone $this;
 
 314                     $oSearch->iSearchRank++;
 
 315                     $oSearch->aAddress[$iWordID] = $iWordID;
 
 316                     $aNewSearches[] = $oSearch;
 
 319                     $this->aFullNameAddress[$iWordID] = $iWordID;
 
 322                 $oSearch = clone $this;
 
 323                 $oSearch->iSearchRank++;
 
 324                 $oSearch->aName = array($iWordID => $iWordID);
 
 325                 $aNewSearches[] = $oSearch;
 
 329         return $aNewSearches;
 
 332     public function extendWithPartialTerm($aSearchTerm, $bStructuredPhrases, $iPhrase, &$aWordFrequencyScores, $aFullTokens)
 
 334         // Only allow name terms.
 
 335         if (!(isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])) {
 
 339         $aNewSearches = array();
 
 340         $iWordID = $aSearchTerm['word_id'];
 
 342         if ((!$bStructuredPhrases || $iPhrase > 0)
 
 343             && sizeof($this->aName)
 
 344             && strpos($aSearchTerm['word_token'], ' ') === false
 
 346             if ($aWordFrequencyScores[$iWordID] < CONST_Max_Word_Frequency) {
 
 347                 $oSearch = clone $this;
 
 348                 $oSearch->iSearchRank++;
 
 349                 $oSearch->aAddress[$iWordID] = $iWordID;
 
 350                 $aNewSearches[] = $oSearch;
 
 352                 $oSearch = clone $this;
 
 353                 $oSearch->iSearchRank++;
 
 354                 $oSearch->aAddressNonSearch[$iWordID] = $iWordID;
 
 355                 if (preg_match('#^[0-9]+$#', $aSearchTerm['word_token'])) {
 
 356                     $oSearch->iSearchRank += 2;
 
 358                 if (sizeof($aFullTokens)) {
 
 359                     $oSearch->iSearchRank++;
 
 361                 $aNewSearches[] = $oSearch;
 
 363                 // revert to the token version?
 
 364                 foreach ($aFullTokens as $aSearchTermToken) {
 
 365                     if (empty($aSearchTermToken['country_code'])
 
 366                         && empty($aSearchTermToken['lat'])
 
 367                         && empty($aSearchTermToken['class'])
 
 369                         $oSearch = clone $this;
 
 370                         $oSearch->iSearchRank++;
 
 371                         $oSearch->aAddress[$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
 
 372                         $aNewSearches[] = $oSearch;
 
 378         if ((!$this->sPostcode && !$this->aAddress && !$this->aAddressNonSearch)
 
 379             && (!sizeof($this->aName) || $this->iNamePhrase == $iPhrase)
 
 381             $oSearch = clone $this;
 
 382             $oSearch->iSearchRank++;
 
 383             if (!sizeof($this->aName)) {
 
 384                 $oSearch->iSearchRank += 1;
 
 386             if (preg_match('#^[0-9]+$#', $aSearchTerm['word_token'])) {
 
 387                 $oSearch->iSearchRank += 2;
 
 389             if ($aWordFrequencyScores[$iWordID] < CONST_Max_Word_Frequency) {
 
 390                 $oSearch->aName[$iWordID] = $iWordID;
 
 392                 $oSearch->aNameNonSearch[$iWordID] = $iWordID;
 
 394             $oSearch->iNamePhrase = $iPhrase;
 
 395             $aNewSearches[] = $oSearch;
 
 398         return $aNewSearches;
 
 401     /////////// Query functions
 
 403     public function queryCountry(&$oDB, $sViewboxSQL)
 
 405         $sSQL = 'SELECT place_id FROM placex ';
 
 406         $sSQL .= "WHERE country_code='".$this->sCountryCode."'";
 
 407         $sSQL .= ' AND rank_search = 4';
 
 409             $sSQL .= " AND ST_Intersects($sViewboxSQL, geometry)";
 
 411         $sSQL .= " ORDER BY st_area(geometry) DESC LIMIT 1";
 
 413         if (CONST_Debug) var_dump($sSQL);
 
 415         return chksql($oDB->getCol($sSQL));
 
 418     public function queryNearbyPoi(&$oDB, $sCountryList, $sViewboxSQL, $sViewboxCentreSQL, $sExcludeSQL, $iLimit)
 
 420         if (!$this->sClass) {
 
 424         $sPoiTable = $this->poiTable();
 
 426         $sSQL = 'SELECT count(*) FROM pg_tables WHERE tablename = \''.$sPoiTable."'";
 
 427         if (chksql($oDB->getOne($sSQL))) {
 
 428             $sSQL = 'SELECT place_id FROM '.$sPoiTable.' ct';
 
 430                 $sSQL .= ' JOIN placex USING (place_id)';
 
 432             if ($this->oNearPoint) {
 
 433                 $sSQL .= ' WHERE '.$this->oNearPoint->withinSQL('ct.centroid');
 
 435                 $sSQL .= " WHERE ST_Contains($sViewboxSQL, ct.centroid)";
 
 438                 $sSQL .= " AND country_code in ($sCountryList)";
 
 441                 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
 
 443             if ($sViewboxCentreSQL) {
 
 444                 $sSQL .= " ORDER BY ST_Distance($sViewboxCentreSQL, ct.centroid) ASC";
 
 445             } elseif ($this->oNearPoint) {
 
 446                 $sSQL .= ' ORDER BY '.$this->oNearPoint->distanceSQL('ct.centroid').' ASC';
 
 448             $sSQL .= " limit $iLimit";
 
 449             if (CONST_Debug) var_dump($sSQL);
 
 450             return chksql($oDB->getCol($sSQL));
 
 453         if ($this->oNearPoint) {
 
 454             $sSQL = 'SELECT place_id FROM placex WHERE ';
 
 455             $sSQL .= 'class=\''.$this->sClass."' and type='".$this->sType."'";
 
 456             $sSQL .= ' AND '.$this->oNearPoint->withinSQL('geometry');
 
 457             $sSQL .= ' AND linked_place_id is null';
 
 459                 $sSQL .= " AND country_code in ($sCountryList)";
 
 461             $sSQL .= ' ORDER BY '.$this->oNearPoint->distanceSQL('centroid')." ASC";
 
 462             $sSQL .= " LIMIT $iLimit";
 
 463             if (CONST_Debug) var_dump($sSQL);
 
 464             return chksql($oDB->getCol($sSQL));
 
 470     public function queryPostcode(&$oDB, $sCountryList, $iLimit)
 
 472         $sSQL  = 'SELECT p.place_id FROM location_postcode p ';
 
 474         if (sizeof($this->aAddress)) {
 
 475             $sSQL .= ', search_name s ';
 
 476             $sSQL .= 'WHERE s.place_id = p.parent_place_id ';
 
 477             $sSQL .= 'AND array_cat(s.nameaddress_vector, s.name_vector)';
 
 478             $sSQL .= '      @> '.getArraySQL($this->aAddress).' AND ';
 
 483         $sSQL .= "p.postcode = '".pg_escape_string(reset($this->$aName))."'";
 
 484         $sCountryTerm = $this->countryCodeSQL('p.country_code', $sCountryList);
 
 486             $sSQL .= ' AND '.$sCountyTerm;
 
 488         $sSQL .= " LIMIT $iLimit";
 
 490         if (CONST_Debug) var_dump($sSQL);
 
 492         return chksql($oDB->getCol($sSQL));
 
 495     public function queryNamedPlace(&$oDB, $aWordFrequencyScores, $sCountryList, $iMinAddressRank, $iMaxAddressRank, $sExcludeSQL, $sViewboxSmall, $sViewboxLarge, $iLimit)
 
 500         if ($this->sHouseNumber && sizeof($this->aAddress)) {
 
 501             $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
 
 503             $aOrder[0] .= 'EXISTS(';
 
 504             $aOrder[0] .= '  SELECT place_id';
 
 505             $aOrder[0] .= '  FROM placex';
 
 506             $aOrder[0] .= '  WHERE parent_place_id = search_name.place_id';
 
 507             $aOrder[0] .= "    AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
 
 508             $aOrder[0] .= '  LIMIT 1';
 
 510             // also housenumbers from interpolation lines table are needed
 
 511             if (preg_match('/[0-9]+/', $this->sHouseNumber)) {
 
 512                 $iHouseNumber = intval($this->sHouseNumber);
 
 513                 $aOrder[0] .= 'OR EXISTS(';
 
 514                 $aOrder[0] .= '  SELECT place_id ';
 
 515                 $aOrder[0] .= '  FROM location_property_osmline ';
 
 516                 $aOrder[0] .= '  WHERE parent_place_id = search_name.place_id';
 
 517                 $aOrder[0] .= '    AND startnumber is not NULL';
 
 518                 $aOrder[0] .= '    AND '.$iHouseNumber.'>=startnumber ';
 
 519                 $aOrder[0] .= '    AND '.$iHouseNumber.'<=endnumber ';
 
 520                 $aOrder[0] .= '  LIMIT 1';
 
 523             $aOrder[0] .= ') DESC';
 
 526         if (sizeof($this->aName)) {
 
 527             $aTerms[] = 'name_vector @> '.getArraySQL($this->aName);
 
 529         if (sizeof($this->aAddress)) {
 
 530             // For infrequent name terms disable index usage for address
 
 531             if (CONST_Search_NameOnlySearchFrequencyThreshold
 
 532                 && sizeof($this->aName) == 1
 
 533                 && $aWordFrequencyScores[$this->aName[reset($this->aName)]]
 
 534                      < CONST_Search_NameOnlySearchFrequencyThreshold
 
 536                 $aTerms[] = 'array_cat(nameaddress_vector,ARRAY[]::integer[]) @> '.getArraySQL($this->aAddress);
 
 538                 $aTerms[] = 'nameaddress_vector @> '.getArraySQL($this->aAddress);
 
 542         $sCountryTerm = $this->countryCodeSQL('country_code', $sCountryList);
 
 544             $aTerms[] = $sCountryTerm;
 
 547         if ($this->sHouseNumber) {
 
 548             $aTerms[] = "address_rank between 16 and 27";
 
 549         } elseif (!$this->sClass || $this->iOperator == Operator::NAME) {
 
 550             if ($iMinAddressRank > 0) {
 
 551                 $aTerms[] = "address_rank >= ".$iMinAddressRank;
 
 553             if ($iMaxAddressRank < 30) {
 
 554                 $aTerms[] = "address_rank <= ".$iMaxAddressRank;
 
 558         if ($this->oNearPoint) {
 
 559             $aTerms[] = $this->oNearPoint->withinSQL('centroid');
 
 560             $aOrder[] = $this->oNearPoint->distanceSQL('centroid');
 
 561         } elseif ($this->sPostcode) {
 
 562             if (!sizeof($this->aAddress)) {
 
 563                 $aTerms[] = "EXISTS(SELECT place_id FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."' AND ST_DWithin(search_name.centroid, p.geometry, 0.1))";
 
 565                 $aOrder[] = "(SELECT min(ST_Distance(search_name.centroid, p.geometry)) FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."')";
 
 570             $aTerms[] = 'place_id not in ('.$sExcludeSQL.')';
 
 573         if ($sViewboxSmall) {
 
 574            $aTerms[] = 'centroid && '.$sViewboxSmall;
 
 577         if ($this->oNearPoint) {
 
 578             $aOrder[] = $this->oNearPoint->distanceSQL('centroid');
 
 581         if ($this->sHouseNumber) {
 
 582             $sImportanceSQL = '- abs(26 - address_rank) + 3';
 
 584             $sImportanceSQL = '(CASE WHEN importance = 0 OR importance IS NULL THEN 0.75-(search_rank::float/40) ELSE importance END)';
 
 586         if ($sViewboxSmall) {
 
 587             $sImportanceSQL .= " * CASE WHEN ST_Contains($sViewboxSmall, centroid) THEN 1 ELSE 0.5 END";
 
 589         if ($sViewboxLarge) {
 
 590             $sImportanceSQL .= " * CASE WHEN ST_Contains($sViewboxLarge, centroid) THEN 1 ELSE 0.5 END";
 
 592         $aOrder[] = "$sImportanceSQL DESC";
 
 594         if (sizeof($this->aFullNameAddress)) {
 
 595             $sExactMatchSQL = ' ( ';
 
 596             $sExactMatchSQL .= ' SELECT count(*) FROM ( ';
 
 597             $sExactMatchSQL .= '  SELECT unnest('.getArraySQL($this->aFullNameAddress).')';
 
 598             $sExactMatchSQL .= '    INTERSECT ';
 
 599             $sExactMatchSQL .= '  SELECT unnest(nameaddress_vector)';
 
 600             $sExactMatchSQL .= ' ) s';
 
 601             $sExactMatchSQL .= ') as exactmatch';
 
 602             $aOrder[] = 'exactmatch DESC';
 
 604             $sExactMatchSQL = '0::int as exactmatch';
 
 607         if ($this->sHouseNumber || $this->sClass) {
 
 611         if (sizeof($aTerms)) {
 
 612             $sSQL = 'SELECT place_id,'.$sExactMatchSQL;
 
 613             $sSQL .= ' FROM search_name';
 
 614             $sSQL .= ' WHERE '.join(' and ', $aTerms);
 
 615             $sSQL .= ' ORDER BY '.join(', ', $aOrder);
 
 616             $sSQL .= ' LIMIT '.$iLimit;
 
 618             if (CONST_Debug) var_dump($sSQL);
 
 622                 "Could not get places for search terms."
 
 630     public function queryHouseNumber(&$oDB, $aRoadPlaceIDs, $sExcludeSQL, $iLimit)
 
 632         $sPlaceIDs = join(',', $aRoadPlaceIDs);
 
 634         $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
 
 635         $sSQL = 'SELECT place_id FROM placex ';
 
 636         $sSQL .= 'WHERE parent_place_id in ('.$sPlaceIDs.')';
 
 637         $sSQL .= "  AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
 
 639             $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
 
 641         $sSQL .= " LIMIT $iLimit";
 
 643         if (CONST_Debug) var_dump($sSQL);
 
 645         $aPlaceIDs = chksql($oDB->getCol($sSQL));
 
 647         if (sizeof($aPlaceIDs)) {
 
 648             return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => -1);
 
 651         $bIsIntHouseNumber= (bool) preg_match('/[0-9]+/', $this->sHouseNumber);
 
 652         $iHousenumber = intval($this->sHouseNumber);
 
 653         if ($bIsIntHouseNumber) {
 
 654             // if nothing found, search in the interpolation line table
 
 655             $sSQL = 'SELECT distinct place_id FROM location_property_osmline';
 
 656             $sSQL .= ' WHERE startnumber is not NULL';
 
 657             $sSQL .= '  AND parent_place_id in ('.$sPlaceIDs.') AND (';
 
 658             if ($iHousenumber % 2 == 0) {
 
 659                 // If housenumber is even, look for housenumber in streets
 
 660                 // with interpolationtype even or all.
 
 661                 $sSQL .= "interpolationtype='even'";
 
 663                 // Else look for housenumber with interpolationtype odd or all.
 
 664                 $sSQL .= "interpolationtype='odd'";
 
 666             $sSQL .= " or interpolationtype='all') and ";
 
 667             $sSQL .= $iHousenumber.">=startnumber and ";
 
 668             $sSQL .= $iHousenumber."<=endnumber";
 
 671                 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
 
 673             $sSQL .= " limit $iLimit";
 
 675             if (CONST_Debug) var_dump($sSQL);
 
 677             $aPlaceIDs = chksql($oDB->getCol($sSQL, 0));
 
 679             if (sizeof($aPlaceIDs)) {
 
 680                 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => $iHousenumber);
 
 684         // If nothing found try the aux fallback table
 
 685         if (CONST_Use_Aux_Location_data) {
 
 686             $sSQL = 'SELECT place_id FROM location_property_aux';
 
 687             $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.')';
 
 688             $sSQL .= " AND housenumber = '".$this->sHouseNumber."'";
 
 690                 $sSQL .= " AND place_id not in ($sExcludeSQL)";
 
 692             $sSQL .= " limit $iLimit";
 
 694             if (CONST_Debug) var_dump($sSQL);
 
 696             $aPlaceIDs = chksql($oDB->getCol($sSQL));
 
 698             if (sizeof($aPlaceIDs)) {
 
 699                 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => -1);
 
 703         // If nothing found then search in Tiger data (location_property_tiger)
 
 704         if (CONST_Use_US_Tiger_Data && $bIsIntHouseNumber) {
 
 705             $sSQL = 'SELECT distinct place_id FROM location_property_tiger';
 
 706             $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.') and (';
 
 707             if ($iHousenumber % 2 == 0) {
 
 708                 $sSQL .= "interpolationtype='even'";
 
 710                 $sSQL .= "interpolationtype='odd'";
 
 712             $sSQL .= " or interpolationtype='all') and ";
 
 713             $sSQL .= $iHousenumber.">=startnumber and ";
 
 714             $sSQL .= $iHousenumber."<=endnumber";
 
 717                 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
 
 719             $sSQL .= " limit $iLimit";
 
 721             if (CONST_Debug) var_dump($sSQL);
 
 723             $aPlaceIDs = chksql($oDB->getCol($sSQL, 0));
 
 725             if (sizeof($aPlaceIDs)) {
 
 726                 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => $iHousenumber);
 
 734     public function queryPoiByOperator(&$oDB, $aParentIDs, $sExcludeSQL, $iLimit)
 
 736         $sPlaceIDs = join(',', $aParentIDs);
 
 737         $aClassPlaceIDs = array();
 
 739         if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NAME) {
 
 740             // If they were searching for a named class (i.e. 'Kings Head pub')
 
 741             // then we might have an extra match
 
 742             $sSQL = 'SELECT place_id FROM placex ';
 
 743             $sSQL .= " WHERE place_id in ($sPlaceIDs)";
 
 744             $sSQL .= "   AND class='".$this->sClass."' ";
 
 745             $sSQL .= "   AND type='".$this->sType."'";
 
 746             $sSQL .= "   AND linked_place_id is null";
 
 747             $sSQL .= " ORDER BY rank_search ASC ";
 
 748             $sSQL .= " LIMIT $iLimit";
 
 750             if (CONST_Debug) var_dump($sSQL);
 
 752             $aClassPlaceIDs = chksql($oDB->getCol($sSQL));
 
 755         // NEAR and IN are handled the same
 
 756         if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NEAR) {
 
 757             $sClassTable = $this->poiTable();
 
 758             $sSQL = "SELECT count(*) FROM pg_tables WHERE tablename = '$sClassTable'";
 
 759             $bCacheTable = (bool) chksql($oDB->getOne($sSQL));
 
 761             $sSQL = "SELECT min(rank_search) FROM placex WHERE place_id in ($sPlaceIDs)";
 
 762             if (CONST_Debug) var_dump($sSQL);
 
 763             $iMaxRank = (int)chksql($oDB->getOne($sSQL));
 
 765             // For state / country level searches the normal radius search doesn't work very well
 
 767             if ($iMaxRank < 9 && $bCacheTable) {
 
 768                 // Try and get a polygon to search in instead
 
 769                 $sSQL = 'SELECT geometry FROM placex';
 
 770                 $sSQL .= " WHERE place_id in ($sPlaceIDs)";
 
 771                 $sSQL .= "   AND rank_search < $iMaxRank + 5";
 
 772                 $sSQL .= "   AND ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon')";
 
 773                 $sSQL .= " ORDER BY rank_search ASC ";
 
 775                 if (CONST_Debug) var_dump($sSQL);
 
 776                 $sPlaceGeom = chksql($oDB->getOne($sSQL));
 
 783                 $sSQL = 'SELECT place_id FROM placex';
 
 784                 $sSQL .= " WHERE place_id in ($sPlaceIDs) and rank_search < $iMaxRank";
 
 785                 if (CONST_Debug) var_dump($sSQL);
 
 786                 $aPlaceIDs = chksql($oDB->getCol($sSQL));
 
 787                 $sPlaceIDs = join(',', $aPlaceIDs);
 
 790             if ($sPlaceIDs || $sPlaceGeom) {
 
 793                     // More efficient - can make the range bigger
 
 797                     if ($this->oNearPoint) {
 
 798                         $sOrderBySQL = $this->oNearPoint->distanceSQL('l.centroid');
 
 799                     } elseif ($sPlaceIDs) {
 
 800                         $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
 
 801                     } elseif ($sPlaceGeom) {
 
 802                         $sOrderBySQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
 
 805                     $sSQL = 'SELECT distinct i.place_id';
 
 807                         $sSQL .= ', i.order_term';
 
 809                     $sSQL .= ' from (SELECT l.place_id';
 
 811                         $sSQL .= ','.$sOrderBySQL.' as order_term';
 
 813                     $sSQL .= ' from '.$sClassTable.' as l';
 
 816                         $sSQL .= ",placex as f WHERE ";
 
 817                         $sSQL .= "f.place_id in ($sPlaceIDs) ";
 
 818                         $sSQL .= " AND ST_DWithin(l.centroid, f.centroid, $fRange)";
 
 819                     } elseif ($sPlaceGeom) {
 
 820                         $sSQL .= " WHERE ST_Contains('$sPlaceGeom', l.centroid)";
 
 824                         $sSQL .= ' AND l.place_id not in ('.$sExcludeSQL.')';
 
 826                     $sSQL .= 'limit 300) i ';
 
 828                         $sSQL .= 'order by order_term asc';
 
 830                     $sSQL .= " limit $iLimit";
 
 832                     if (CONST_Debug) var_dump($sSQL);
 
 834                     $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($oDB->getCol($sSQL)));
 
 836                     if ($this->oNearPoint) {
 
 837                         $fRange = $this->oNearPoint->radius();
 
 841                     if ($this->oNearPoint) {
 
 842                         $sOrderBySQL = $this->oNearPoint->distanceSQL('l.geometry');
 
 844                         $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
 
 847                     $sSQL = 'SELECT distinct l.place_id';
 
 849                         $sSQL .= ','.$sOrderBySQL.' as orderterm';
 
 851                     $sSQL .= ' FROM placex as l, placex as f';
 
 852                     $sSQL .= " WHERE f.place_id in ($sPlaceIDs)";
 
 853                     $sSQL .= "  AND ST_DWithin(l.geometry, f.centroid, $fRange)";
 
 854                     $sSQL .= "  AND l.class='".$this->sClass."'";
 
 855                     $sSQL .= "  AND l.type='".$this->sType."'";
 
 857                         $sSQL .= " AND l.place_id not in (".$sExcludeSQL.")";
 
 860                         $sSQL .= "ORDER BY orderterm ASC";
 
 862                     $sSQL .= " limit $iLimit";
 
 864                     if (CONST_Debug) var_dump($sSQL);
 
 866                     $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($oDB->getCol($sSQL)));
 
 871         return $aClassPlaceIDs;
 
 875     /////////// Sort functions
 
 877     static function bySearchRank($a, $b)
 
 879         if ($a->iSearchRank == $b->iSearchRank) {
 
 880             return $a->iOperator + strlen($a->sHouseNumber)
 
 881                      - $b->iOperator - strlen($b->sHouseNumber);
 
 884         return $a->iSearchRank < $b->iSearchRank ? -1 : 1;