]> git.openstreetmap.org Git - nominatim.git/blobdiff - lib-php/SearchDescription.php
remove Token from explicit input for SearchDescription extension
[nominatim.git] / lib-php / SearchDescription.php
index 94cadda322ce5e1f113555bf71495d2b69028221..b4a78eb8e70a9e7274460340e4c76e23db53f91c 100644 (file)
@@ -152,23 +152,17 @@ class SearchDescription
     /**
      * Derive new searches by adding a full term to the existing search.
      *
-     * @param object $oSearchTerm  Description of the token.
-     * @param bool   $bHasPartial  True if there are also tokens of partial terms
-     *                             with the same name.
-     * @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   $bFirstPhrase True if the token is in the first phrase of
-     *                             the query.
-     * @param bool   $bLastToken   True if the token is at the end of the query.
+     * @param object  $oSearchTerm  Description of the token.
+     * @param object  $oPosition    Description of the token position within
+                                    the query.
      *
      * @return SearchDescription[] List of derived search descriptions.
      */
-    public function extendWithFullTerm($oSearchTerm, $bHasPartial, $sPhraseType, $bFirstToken, $bFirstPhrase, $bLastToken)
+    public function extendWithSearchTerm($oSearchTerm, $oPosition)
     {
         $aNewSearches = array();
 
-        if (($sPhraseType == '' || $sPhraseType == 'country')
+        if ($oPosition->maybePhrase('country')
             && is_a($oSearchTerm, '\Nominatim\Token\Country')
         ) {
             if (!$this->sCountryCode) {
@@ -177,18 +171,19 @@ class SearchDescription
                 $oSearch->sCountryCode = $oSearchTerm->sCountryCode;
                 // Country is almost always at the end of the string
                 // - increase score for finding it anywhere else (optimisation)
-                if (!$bLastToken) {
+                if (!$oPosition->isLastToken()) {
                     $oSearch->iSearchRank += 5;
+                    $oSearch->iNamePhrase = -1;
                 }
                 $aNewSearches[] = $oSearch;
             }
-        } elseif (($sPhraseType == '' || $sPhraseType == 'postalcode')
+        } elseif ($oPosition->maybePhrase('postalcode')
                   && is_a($oSearchTerm, '\Nominatim\Token\Postcode')
         ) {
             if (!$this->sPostcode) {
                 // If we have structured search or this is the first term,
                 // make the postcode the primary search element.
-                if ($this->iOperator == Operator::NONE && $bFirstToken) {
+                if ($this->iOperator == Operator::NONE && $oPosition->isFirstToken()) {
                     $oSearch = clone $this;
                     $oSearch->iSearchRank++;
                     $oSearch->iOperator = Operator::POSTCODE;
@@ -201,10 +196,11 @@ class SearchDescription
                 // If we have a structured search or this is not the first term,
                 // add the postcode as an addendum.
                 if ($this->iOperator != Operator::POSTCODE
-                    && ($sPhraseType == 'postalcode' || !empty($this->aName))
+                    && ($oPosition->isPhrase('postalcode') || !empty($this->aName))
                 ) {
                     $oSearch = clone $this;
                     $oSearch->iSearchRank++;
+                    $oSearch->iNamePhrase = -1;
                     if (strlen($oSearchTerm->sPostcode) < 4) {
                         $oSearch->iSearchRank += 4 - strlen($oSearchTerm->sPostcode);
                     }
@@ -212,33 +208,37 @@ class SearchDescription
                     $aNewSearches[] = $oSearch;
                 }
             }
-        } elseif (($sPhraseType == '' || $sPhraseType == 'street')
+        } elseif ($oPosition->maybePhrase('street')
                  && is_a($oSearchTerm, '\Nominatim\Token\HouseNumber')
         ) {
             if (!$this->sHouseNumber && $this->iOperator != Operator::POSTCODE) {
-                $oSearch = clone $this;
-                $oSearch->iSearchRank++;
-                $oSearch->sHouseNumber = $oSearchTerm->sToken;
-                if ($this->iOperator != Operator::NONE) {
-                    $oSearch->iSearchRank++;
-                }
                 // sanity check: if the housenumber is not mainly made
                 // up of numbers, add a penalty
-                if (preg_match('/\\d/', $oSearch->sHouseNumber) === 0
-                    || preg_match_all('/[^0-9]/', $oSearch->sHouseNumber, $aMatches) > 2) {
-                    $oSearch->iSearchRank++;
+                $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++;
                 }
                 if (empty($oSearchTerm->iId)) {
-                    $oSearch->iSearchRank++;
+                    $iSearchCost++;
                 }
                 // also must not appear in the middle of the address
                 if (!empty($this->aAddress)
                     || (!empty($this->aAddressNonSearch))
                     || $this->sPostcode
                 ) {
-                    $oSearch->iSearchRank++;
+                    $iSearchCost++;
                 }
+
+                $oSearch = clone $this;
+                $oSearch->iSearchRank += $iSearchCost;
+                $oSearch->iNamePhrase = -1;
+                $oSearch->sHouseNumber = $oSearchTerm->sToken;
                 $aNewSearches[] = $oSearch;
+
                 // Housenumbers may appear in the name when the place has its own
                 // address terms.
                 if ($oSearchTerm->iId !== null
@@ -246,19 +246,20 @@ class SearchDescription
                     && empty($this->aAddress)
                    ) {
                     $oSearch = clone $this;
-                    $oSearch->iSearchRank++;
+                    $oSearch->iSearchRank += $iSearchCost;
                     $oSearch->aAddress = $this->aName;
                     $oSearch->bRareName = false;
                     $oSearch->aName = array($oSearchTerm->iId => $oSearchTerm->iId);
                     $aNewSearches[] = $oSearch;
                 }
             }
-        } elseif ($sPhraseType == ''
+        } elseif ($oPosition->isPhrase('')
                   && is_a($oSearchTerm, '\Nominatim\Token\SpecialTerm')
         ) {
             if ($this->iOperator == Operator::NONE) {
                 $oSearch = clone $this;
                 $oSearch->iSearchRank += 2;
+                $oSearch->iNamePhrase = -1;
 
                 $iOp = $oSearchTerm->iOperator;
                 if ($iOp == Operator::NONE) {
@@ -268,7 +269,7 @@ class SearchDescription
                         $iOp = Operator::NEAR;
                     }
                     $oSearch->iSearchRank += 2;
-                } else if (!$bFirstToken && !$bLastToken) {
+                } elseif (!$oPosition->isFirstToken() && !$oPosition->isLastToken()) {
                     $oSearch->iSearchRank += 2;
                 }
                 if ($this->sHouseNumber) {
@@ -282,7 +283,7 @@ class SearchDescription
                 );
                 $aNewSearches[] = $oSearch;
             }
-        } elseif ($sPhraseType != 'country'
+        } elseif (!$oPosition->isPhrase('country')
                   && is_a($oSearchTerm, '\Nominatim\Token\Word')
         ) {
             $iWordID = $oSearchTerm->iId;
@@ -290,14 +291,17 @@ class SearchDescription
             // 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.
-            if (!empty($this->aName) || !($bFirstPhrase || $sPhraseType == '')) {
-                if (($sPhraseType == '' || !$bFirstPhrase) && !$bHasPartial) {
+            if (!empty($this->aName) || !($oPosition->isFirstPhrase() || $oPosition->isPhrase(''))) {
+                if (($oPosition->isPhrase('') || !$oPosition->isFirstPhrase())
+                    && $oSearchTerm->iTermCount > 1
+                ) {
                     $oSearch = clone $this;
-                    $oSearch->iSearchRank += 3 * $oSearchTerm->iTermCount;
+                    $oSearch->iNamePhrase = -1;
+                    $oSearch->iSearchRank += 1;
                     $oSearch->aAddress[$iWordID] = $iWordID;
                     $aNewSearches[] = $oSearch;
                 }
-            } else if (empty($this->aNameNonSearch)) {
+            } elseif (empty($this->aNameNonSearch)) {
                 $oSearch = clone $this;
                 $oSearch->iSearchRank++;
                 $oSearch->aName = array($iWordID => $iWordID);
@@ -308,6 +312,13 @@ class SearchDescription
                 }
                 $aNewSearches[] = $oSearch;
             }
+        } elseif (!$oPosition->isPhrase('country')
+                  && is_a($oSearchTerm, '\Nominatim\Token\Partial')
+        ) {
+            $aNewSearches = $this->extendWithPartialTerm(
+                $oSearchTerm,
+                $oPosition
+            );
         }
 
         return $aNewSearches;
@@ -316,53 +327,43 @@ class SearchDescription
     /**
      * Derive new searches by adding a partial term to the existing search.
      *
-     * @param string  $sToken             Term for the token.
-     * @param object  $oSearchTerm        Description of the token.
-     * @param bool    $bStructuredPhrases True if the search is structured.
-     * @param integer $iPhrase            Number of the phrase the token is in.
-     * @param array[] $aFullTokens        List of full term tokens with the
-     *                                    same name.
+     * @param object  $oSearchTerm  Description of the token.
+     * @param object  $oPosition    Description of the token position within
+                                    the query.
      *
      * @return SearchDescription[] List of derived search descriptions.
      */
-    public function extendWithPartialTerm($sToken, $oSearchTerm, $bStructuredPhrases, $iPhrase, $aFullTokens)
+    private function extendWithPartialTerm($oSearchTerm, $oPosition)
     {
-        // Only allow name terms.
-        if (!(is_a($oSearchTerm, '\Nominatim\Token\Word'))) {
-            return array();
-        }
-
         $aNewSearches = array();
         $iWordID = $oSearchTerm->iId;
 
-        if ((!$bStructuredPhrases || $iPhrase > 0)
+        if (($oPosition->isPhrase('') || !$oPosition->isFirstPhrase())
             && (!empty($this->aName))
         ) {
             $oSearch = clone $this;
             $oSearch->iSearchRank++;
-            if (preg_match('#^[0-9 ]+$#', $sToken)) {
+            if (preg_match('#^[0-9 ]+$#', $oSearchTerm->sToken)) {
                 $oSearch->iSearchRank++;
             }
             if ($oSearchTerm->iSearchNameCount < CONST_Max_Word_Frequency) {
                 $oSearch->aAddress[$iWordID] = $iWordID;
             } else {
                 $oSearch->aAddressNonSearch[$iWordID] = $iWordID;
-                if (!empty($aFullTokens)) {
-                    $oSearch->iSearchRank++;
-                }
             }
             $aNewSearches[] = $oSearch;
         }
 
         if ((!$this->sPostcode && !$this->aAddress && !$this->aAddressNonSearch)
-            && ((empty($this->aName) && empty($this->aNameNonSearch)) || $this->iNamePhrase == $iPhrase)
+            && ((empty($this->aName) && empty($this->aNameNonSearch))
+                || $this->iNamePhrase == $oPosition->getPhrase())
         ) {
             $oSearch = clone $this;
             $oSearch->iSearchRank++;
             if (empty($this->aName) && empty($this->aNameNonSearch)) {
                 $oSearch->iSearchRank++;
             }
-            if (preg_match('#^[0-9 ]+$#', $sToken)) {
+            if (preg_match('#^[0-9 ]+$#', $oSearchTerm->sToken)) {
                 $oSearch->iSearchRank++;
             }
             if ($oSearchTerm->iSearchNameCount < CONST_Max_Word_Frequency) {
@@ -377,12 +378,9 @@ class SearchDescription
                 }
                 $oSearch->aName[$iWordID] = $iWordID;
             } else {
-                if (!empty($aFullTokens)) {
-                    $oSearch->iSearchRank++;
-                }
                 $oSearch->aNameNonSearch[$iWordID] = $iWordID;
             }
-            $oSearch->iNamePhrase = $iPhrase;
+            $oSearch->iNamePhrase = $oPosition->getPhrase();
             $aNewSearches[] = $oSearch;
         }
 
@@ -407,7 +405,6 @@ class SearchDescription
     public function query(&$oDB, $iMinRank, $iMaxRank, $iLimit)
     {
         $aResults = array();
-        $iHousenumber = -1;
 
         if ($this->sCountryCode
             && empty($this->aName)
@@ -440,23 +437,24 @@ class SearchDescription
 
             // Now search for housenumber, if housenumber provided. Can be zero.
             if (($this->sHouseNumber || $this->sHouseNumber === '0') && !empty($aResults)) {
+                $aHnResults = $this->queryHouseNumber($oDB, $aResults);
+
                 // Downgrade the rank of the street results, they are missing
-                // the housenumber.
+                // 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.
                 foreach ($aResults as $oRes) {
-                    if ($oRes->iAddressRank >= 26) {
-                        $oRes->iResultRank++;
-                    } else {
-                        $oRes->iResultRank += 2;
+                    if ($oRes->iAddressRank < 28) {
+                        if ($oRes->iAddressRank >= 26) {
+                            $oRes->iResultRank++;
+                        } else {
+                            $oRes->iResultRank += 2;
+                        }
+                        $aHnResults[$oRes->iId] = $oRes;
                     }
                 }
 
-                $aHnResults = $this->queryHouseNumber($oDB, $aResults);
-
-                if (!empty($aHnResults)) {
-                    foreach ($aHnResults as $oRes) {
-                        $aResults[$oRes->iId] = $oRes;
-                    }
-                }
+                $aResults = $aHnResults;
             }
 
             // finally get POIs if requested
@@ -608,14 +606,14 @@ class SearchDescription
         // 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.
-        if ($this->sHouseNumber && (!empty($this->aAddress) || $this->sPostcode)) {
+        if ($this->sHouseNumber && ($this->bRareName || !empty($this->aAddress) || $this->sPostcode)) {
             $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';
-            $aOrder[0] .= "    AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
+            $aOrder[0] .= "    AND housenumber ~* E'".$sHouseNumberRegex."'";
             $aOrder[0] .= '  LIMIT 1';
             $aOrder[0] .= ') ';
             // also housenumbers from interpolation lines table are needed
@@ -736,16 +734,33 @@ class SearchDescription
     private function queryHouseNumber(&$oDB, $aRoadPlaceIDs)
     {
         $aResults = array();
-        $sPlaceIDs = Result::joinIdsByTable($aRoadPlaceIDs, Result::TABLE_PLACEX);
+        $sRoadPlaceIDs = Result::joinIdsByTableMaxRank(
+            $aRoadPlaceIDs,
+            Result::TABLE_PLACEX,
+            27
+        );
+        $sPOIPlaceIDs = Result::joinIdsByTableMinRank(
+            $aRoadPlaceIDs,
+            Result::TABLE_PLACEX,
+            30
+        );
 
-        if (!$sPlaceIDs) {
+        $aIDCondition = array();
+        if ($sRoadPlaceIDs) {
+            $aIDCondition[] = 'parent_place_id in ('.$sRoadPlaceIDs.')';
+        }
+        if ($sPOIPlaceIDs) {
+            $aIDCondition[] = 'place_id in ('.$sPOIPlaceIDs.')';
+        }
+
+        if (empty($aIDCondition)) {
             return $aResults;
         }
 
         $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
-        $sSQL = 'SELECT place_id FROM placex ';
-        $sSQL .= 'WHERE parent_place_id in ('.$sPlaceIDs.')';
-        $sSQL .= "  AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
+        $sSQL = 'SELECT place_id FROM placex WHERE';
+        $sSQL .= "  housenumber ~* E'".$sHouseNumberRegex."'";
+        $sSQL .= ' AND ('.join(' OR ', $aIDCondition).')';
         $sSQL .= $this->oContext->excludeSQL(' AND place_id');
 
         Debug::printSQL($sSQL);
@@ -757,11 +772,11 @@ class SearchDescription
 
         $bIsIntHouseNumber= (bool) preg_match('/[0-9]+/', $this->sHouseNumber);
         $iHousenumber = intval($this->sHouseNumber);
-        if ($bIsIntHouseNumber && empty($aResults)) {
+        if ($bIsIntHouseNumber && $sRoadPlaceIDs && empty($aResults)) {
             // if nothing found, search in the interpolation line table
             $sSQL = 'SELECT distinct place_id FROM location_property_osmline';
             $sSQL .= ' WHERE startnumber is not NULL';
-            $sSQL .= '  AND parent_place_id in ('.$sPlaceIDs.') AND (';
+            $sSQL .= '  AND parent_place_id in ('.$sRoadPlaceIDs.') AND (';
             if ($iHousenumber % 2 == 0) {
                 // If housenumber is even, look for housenumber in streets
                 // with interpolationtype even or all.
@@ -784,24 +799,10 @@ class SearchDescription
             }
         }
 
-        // If nothing found try the aux fallback table
-        if (CONST_Use_Aux_Location_data && empty($aResults)) {
-            $sSQL = 'SELECT place_id FROM location_property_aux';
-            $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.')';
-            $sSQL .= " AND housenumber = '".$this->sHouseNumber."'";
-            $sSQL .= $this->oContext->excludeSQL(' AND place_id');
-
-            Debug::printSQL($sSQL);
-
-            foreach ($oDB->getCol($sSQL) as $iPlaceId) {
-                $aResults[$iPlaceId] = new Result($iPlaceId, Result::TABLE_AUX);
-            }
-        }
-
         // If nothing found then search in Tiger data (location_property_tiger)
-        if (CONST_Use_US_Tiger_Data && $bIsIntHouseNumber && empty($aResults)) {
+        if (CONST_Use_US_Tiger_Data && $sRoadPlaceIDs && $bIsIntHouseNumber && empty($aResults)) {
             $sSQL = 'SELECT place_id FROM location_property_tiger';
-            $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.') and (';
+            $sSQL .= ' WHERE parent_place_id in ('.$sRoadPlaceIDs.') and (';
             if ($iHousenumber % 2 == 0) {
                 $sSQL .= "interpolationtype='even'";
             } else {