]> git.openstreetmap.org Git - nominatim.git/blobdiff - lib-php/SearchDescription.php
Merge remote-tracking branch 'upstream/master'
[nominatim.git] / lib-php / SearchDescription.php
index 4886462aa6bcdc6c9ac1e9725d584699fcf0b9a5..be5623afcd3170ec58662b7e7c9ea6f8258adb83 100644 (file)
@@ -19,6 +19,8 @@ class SearchDescription
     private $aName = array();
     /// True if the name is rare enough to force index use on name.
     private $bRareName = false;
+    /// True if the name requires to be accompanied by address terms.
+    private $bNameNeedsAddress = false;
     /// List of word ids making up the address of the object.
     private $aAddress = array();
     /// List of word ids that appear in the name but should be ignored.
@@ -113,11 +115,22 @@ class SearchDescription
                 return false;
             }
         }
+        if ($this->bNameNeedsAddress && empty($this->aAddress)) {
+            return false;
+        }
 
         return true;
     }
 
     /////////// Search building functions
+
+    /**
+     * Create a copy of this search description adding to search rank.
+     *
+     * @param integer $iTermCost  Cost to add to the current search rank.
+     *
+     * @return object Cloned search description.
+     */
     public function clone($iTermCost)
     {
         $oSearch = clone $this;
@@ -126,37 +139,82 @@ class SearchDescription
         return $oSearch;
     }
 
+    /**
+     * Check if the search currently includes a name.
+     *
+     * @param bool bIncludeNonNames  If true stop-word tokens are taken into
+     *                               account, too.
+     *
+     * @return bool True, if search has a name.
+     */
     public function hasName($bIncludeNonNames = false)
     {
         return !empty($this->aName)
                || (!empty($this->aNameNonSearch) && $bIncludeNonNames);
     }
 
+    /**
+     * Check if the search currently includes an address term.
+     *
+     * @return bool True, if any address term is included, including stop-word
+     *              terms.
+     */
     public function hasAddress()
     {
         return !empty($this->aAddress) || !empty($this->aAddressNonSearch);
     }
 
+    /**
+     * Check if a country restriction is currently included in the search.
+     *
+     * @return bool True, if a country restriction is set.
+     */
     public function hasCountry()
     {
         return $this->sCountryCode !== '';
     }
 
+    /**
+     * Check if a postcode is currently included in the search.
+     *
+     * @return bool True, if a postcode is set.
+     */
     public function hasPostcode()
     {
         return $this->sPostcode !== '';
     }
 
+    /**
+     * Check if a house number is set for the search.
+     *
+     * @return bool True, if a house number is set.
+     */
     public function hasHousenumber()
     {
         return $this->sHouseNumber !== '';
     }
 
+    /**
+     * Check if a special type of place is requested.
+     *
+     * param integer iOperator  When set, check for the particular
+     *                          operator used for the special type.
+     *
+     * @return bool True, if speial type is requested or, if requested,
+     *              a special type with the given operator.
+     */
     public function hasOperator($iOperator = null)
     {
         return $iOperator === null ? $this->iOperator != Operator::NONE : $this->iOperator == $iOperator;
     }
 
+    /**
+     * Add the given token to the list of terms to search for in the address.
+     *
+     * @param integer iID       ID of term to add.
+     * @param bool bSearchable  Term should be used to search for result
+     *                          (i.e. term is not a stop word).
+     */
     public function addAddressToken($iId, $bSearchable = true)
     {
         if ($bSearchable) {
@@ -166,13 +224,41 @@ class SearchDescription
         }
     }
 
-    public function addNameToken($iId)
+    /**
+     * Add the given full-word token to the list of terms to search for in the
+     * name.
+     *
+     * @param interger iId    ID of term to add.
+     * @param bool bRareName  True if the term is infrequent enough to not
+     *                        require other constraints for efficient search.
+     */
+    public function addNameToken($iId, $bRareName)
     {
         $this->aName[$iId] = $iId;
+        $this->bRareName = $bRareName;
+        $this->bNameNeedsAddress = false;
     }
 
-    public function addPartialNameToken($iId, $bSearchable, $iPhraseNumber)
+    /**
+     * Add the given partial token to the list of terms to search for in
+     * the name.
+     *
+     * @param integer iID            ID of term to add.
+     * @param bool bSearchable       Term should be used to search for result
+     *                               (i.e. term is not a stop word).
+     * @param bool bNeedsAddress     True if the term is too unspecific to be used
+     *                               in a stand-alone search without an address
+     *                               to narrow down the search.
+     * @param integer iPhraseNumber  Index of phrase, where the partial term
+     *                               appears.
+     */
+    public function addPartialNameToken($iId, $bSearchable, $bNeedsAddress, $iPhraseNumber)
     {
+        if (empty($this->aName)) {
+            $this->bNameNeedsAddress = $bNeedsAddress;
+        } else {
+            $this->bNameNeedsAddress &= $bNeedsAddress;
+        }
         if ($bSearchable) {
             $this->aName[$iId] = $iId;
         } else {
@@ -181,23 +267,34 @@ class SearchDescription
         $this->iNamePhrase = $iPhraseNumber;
     }
 
-    public function markRareName()
-    {
-        $this->bRareName = true;
-    }
-
+    /**
+     * Set country restriction for the search.
+     *
+     * @param string sCountryCode  Country code of country to restrict search to.
+     */
     public function setCountry($sCountryCode)
     {
         $this->sCountryCode = $sCountryCode;
         $this->iNamePhrase = -1;
     }
 
+    /**
+     * Set postcode search constraint.
+     *
+     * @param string sPostcode  Postcode the result should have.
+     */
     public function setPostcode($sPostcode)
     {
         $this->sPostcode = $sPostcode;
         $this->iNamePhrase = -1;
     }
 
+    /**
+     * Make this search a search for a postcode object.
+     *
+     * @param integer iId       Token Id for the postcode.
+     * @param string sPostcode  Postcode to look for.
+     */
     public function setPostcodeAsName($iId, $sPostcode)
     {
         $this->iOperator = Operator::POSTCODE;
@@ -207,16 +304,27 @@ class SearchDescription
         $this->iNamePhrase = -1;
     }
 
+    /**
+     * Set house number search cnstraint.
+     *
+     * @param string sNumber  House number the result should have.
+     */
     public function setHousenumber($sNumber)
     {
         $this->sHouseNumber = $sNumber;
         $this->iNamePhrase = -1;
     }
 
+    /**
+     * Make this search a search for a house number.
+     *
+     * @param integer iId  Token Id for the house number.
+     */
     public function setHousenumberAsName($iId)
     {
         $this->aAddress = array_merge($this->aAddress, $this->aName);
         $this->bRareName = false;
+        $this->bNameNeedsAddress = true;
         $this->aName = array($iId => $iId);
         $this->iNamePhrase = -1;
     }
@@ -246,6 +354,11 @@ class SearchDescription
         return $this->iNamePhrase;
     }
 
+    /**
+     * Get the global search context.
+     *
+     * @return object  Objects of global search constraints.
+     */
     public function getContext()
     {
         return $this->oContext;
@@ -468,32 +581,40 @@ class SearchDescription
 
         // Sort by existence of the requested house number but only if not
         // too many results are expected for the street, i.e. if the result
-        // will be narrowed down by an address. Remeber that with ordering
+        // will be narrowed down by an address. Remember that with ordering
         // every single result has to be checked.
         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 housenumber ~* E'".$sHouseNumberRegex."'";
-            $aOrder[0] .= '  LIMIT 1';
-            $aOrder[0] .= ') ';
-            // also housenumbers from interpolation lines table are needed
-            if (preg_match('/[0-9]+/', $this->sHouseNumber)) {
-                $iHouseNumber = intval($this->sHouseNumber);
-                $aOrder[0] .= 'OR EXISTS(';
-                $aOrder[0] .= '  SELECT place_id ';
-                $aOrder[0] .= '  FROM location_property_osmline ';
-                $aOrder[0] .= '  WHERE parent_place_id = search_name.place_id';
-                $aOrder[0] .= '    AND startnumber is not NULL';
-                $aOrder[0] .= '    AND '.$iHouseNumber.'>=startnumber ';
-                $aOrder[0] .= '    AND '.$iHouseNumber.'<=endnumber ';
-                $aOrder[0] .= '  LIMIT 1';
-                $aOrder[0] .= ')';
+            $sHouseNumberRegex = $oDB->getDBQuoted('\\\\m'.$this->sHouseNumber.'\\\\M');
+
+            // Housenumbers on streets and places.
+            $sChildHnr = 'SELECT * FROM placex WHERE parent_place_id = search_name.place_id';
+            $sChildHnr .= '    AND housenumber ~* E'.$sHouseNumberRegex;
+            // Interpolations on streets and places.
+            if (preg_match('/^[0-9]+$/', $this->sHouseNumber)) {
+                $sIpolHnr = 'WHERE parent_place_id = search_name.place_id ';
+                $sIpolHnr .= '  AND startnumber is not NULL';
+                $sIpolHnr .= '    AND '.$this->sHouseNumber.'>=startnumber ';
+                $sIpolHnr .= '    AND '.$this->sHouseNumber.'<=endnumber ';
+            } else {
+                $sIpolHnr = false;
             }
-            $aOrder[0] .= ') DESC';
+            // Housenumbers on the object iteself for unlisted places.
+            $sSelfHnr = 'SELECT * FROM placex WHERE place_id = search_name.place_id';
+            $sSelfHnr .= '    AND housenumber ~* E'.$sHouseNumberRegex;
+
+            $sSql = '(CASE WHEN address_rank = 30 THEN EXISTS('.$sSelfHnr.') ';
+            $sSql .= ' ELSE EXISTS('.$sChildHnr.') ';
+            if ($sIpolHnr) {
+                $sSql .= 'OR EXISTS(SELECT * FROM location_property_osmline '.$sIpolHnr.') ';
+                if (CONST_Use_US_Tiger_Data) {
+                    $sSql .= "OR (country_code = 'us' AND ";
+                    $sSql .= '    EXISTS(SELECT * FROM location_property_tiger '.$sIpolHnr.')) ';
+                }
+            }
+            $sSql .= 'END) DESC';
+
+
+            $aOrder[] = $sSql;
         }
 
         if (!empty($this->aName)) {
@@ -526,7 +647,7 @@ class SearchDescription
             $aOrder[] = $this->oContext->distanceSQL('centroid');
         } elseif ($this->sPostcode) {
             if (empty($this->aAddress)) {
-                $aTerms[] = "EXISTS(SELECT place_id FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."' AND ST_DWithin(search_name.centroid, p.geometry, 0.1))";
+                $aTerms[] = "EXISTS(SELECT place_id FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."' AND ST_DWithin(search_name.centroid, p.geometry, 0.12))";
             } else {
                 $aOrder[] = "(SELECT min(ST_Distance(search_name.centroid, p.geometry)) FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."')";
             }
@@ -621,9 +742,9 @@ class SearchDescription
             return $aResults;
         }
 
-        $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
+        $sHouseNumberRegex = $oDB->getDBQuoted('\\\\m'.$this->sHouseNumber.'\\\\M');
         $sSQL = 'SELECT place_id FROM placex WHERE';
-        $sSQL .= "  housenumber ~* E'".$sHouseNumberRegex."'";
+        $sSQL .= '  housenumber ~* E'.$sHouseNumberRegex;
         $sSQL .= ' AND ('.join(' OR ', $aIDCondition).')';
         $sSQL .= $this->oContext->excludeSQL(' AND place_id');