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.
return $this->iSearchRank;
}
- /**
- * Make this search a POI search.
- *
- * In a POI search, objects are not (only) searched by their name
- * but also by the primary OSM key/value pair (class and type in Nominatim).
- *
- * @param integer $iOperator Type of POI search
- * @param string $sClass Class (or OSM tag key) of POI.
- * @param string $sType Type (or OSM tag value) of POI.
- *
- * @return void
- */
- public function setPoiSearch($iOperator, $sClass, $sType)
- {
- $this->iOperator = $iOperator;
- $this->sClass = $sClass;
- $this->sType = $sType;
- }
-
- /**
- * Check if any operator is set.
- *
- * @return bool True, if this is a special search operation.
- */
- public function hasOperator()
- {
- return $this->iOperator != Operator::NONE;
- }
-
/**
* Extract key/value pairs from a query.
*
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;
+ $oSearch->iSearchRank += $iTermCost;
+
+ return $oSearch;
+ }
/**
- * Derive new searches by adding a full term to the existing search.
+ * Check if the search currently includes a name.
*
- * @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 bool bIncludeNonNames If true stop-word tokens are taken into
+ * account, too.
*
- * @return SearchDescription[] List of derived search descriptions.
+ * @return bool True, if search has a name.
*/
- public function extendWithFullTerm($oSearchTerm, $bHasPartial, $sPhraseType, $bFirstToken, $bFirstPhrase, $bLastToken)
+ public function hasName($bIncludeNonNames = false)
{
- $aNewSearches = array();
+ return !empty($this->aName)
+ || (!empty($this->aNameNonSearch) && $bIncludeNonNames);
+ }
- if (($sPhraseType == '' || $sPhraseType == 'country')
- && is_a($oSearchTerm, '\Nominatim\Token\Country')
- ) {
- if (!$this->sCountryCode) {
- $oSearch = clone $this;
- $oSearch->iSearchRank++;
- $oSearch->sCountryCode = $oSearchTerm->sCountryCode;
- // Country is almost always at the end of the string
- // - increase score for finding it anywhere else (optimisation)
- if (!$bLastToken) {
- $oSearch->iSearchRank += 5;
- }
- $aNewSearches[] = $oSearch;
- }
- } elseif (($sPhraseType == '' || $sPhraseType == '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) {
- $oSearch = clone $this;
- $oSearch->iSearchRank++;
- $oSearch->iOperator = Operator::POSTCODE;
- $oSearch->aAddress = array_merge($this->aAddress, $this->aName);
- $oSearch->aName =
- array($oSearchTerm->iId => $oSearchTerm->sPostcode);
- $aNewSearches[] = $oSearch;
- }
+ /**
+ * 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);
+ }
- // 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))
- ) {
- $oSearch = clone $this;
- $oSearch->iSearchRank++;
- if (strlen($oSearchTerm->sPostcode) < 4) {
- $oSearch->iSearchRank += 4 - strlen($oSearchTerm->sPostcode);
- }
- $oSearch->sPostcode = $oSearchTerm->sPostcode;
- $aNewSearches[] = $oSearch;
- }
- }
- } elseif (($sPhraseType == '' || $sPhraseType == 'street')
- && is_a($oSearchTerm, '\Nominatim\Token\HouseNumber')
- ) {
- if (!$this->sHouseNumber && $this->iOperator != Operator::POSTCODE) {
- $oSearch = clone $this;
- $oSearch->iSearchRank++;
- $oSearch->sHouseNumber = $oSearchTerm->sToken;
- // 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++;
- }
- if (empty($oSearchTerm->iId)) {
- $oSearch->iSearchRank++;
- }
- // also must not appear in the middle of the address
- if (!empty($this->aAddress)
- || (!empty($this->aAddressNonSearch))
- || $this->sPostcode
- ) {
- $oSearch->iSearchRank++;
- }
- $aNewSearches[] = $oSearch;
- // Housenumbers may appear in the name when the place has its own
- // address terms.
- if ($oSearchTerm->iId !== null
- && ($this->iNamePhrase >= 0 || empty($this->aName))
- && empty($this->aAddress)
- ) {
- $oSearch = clone $this;
- $oSearch->iSearchRank++;
- $oSearch->aAddress = $this->aName;
- $oSearch->bRareName = false;
- $oSearch->aName = array($oSearchTerm->iId => $oSearchTerm->iId);
- $aNewSearches[] = $oSearch;
- }
- }
- } elseif ($sPhraseType == ''
- && is_a($oSearchTerm, '\Nominatim\Token\SpecialTerm')
- ) {
- if ($this->iOperator == Operator::NONE) {
- $oSearch = clone $this;
- $oSearch->iSearchRank++;
-
- $iOp = $oSearchTerm->iOperator;
- if ($iOp == Operator::NONE) {
- if (!empty($this->aName) || $this->oContext->isBoundedSearch()) {
- $iOp = Operator::NAME;
- } else {
- $iOp = Operator::NEAR;
- }
- $oSearch->iSearchRank += 2;
- }
+ /**
+ * 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 !== '';
+ }
- $oSearch->setPoiSearch(
- $iOp,
- $oSearchTerm->sClass,
- $oSearchTerm->sType
- );
- $aNewSearches[] = $oSearch;
- }
- } elseif ($sPhraseType != 'country'
- && is_a($oSearchTerm, '\Nominatim\Token\Word')
- ) {
- $iWordID = $oSearchTerm->iId;
- // Full words can only be a name if they appear at the beginning
- // of the phrase. In structured search the name must forcably in
- // the first phrase. In unstructured search it may be in a later
- // phrase when the first phrase is a house number.
- if (!empty($this->aName) || !($bFirstPhrase || $sPhraseType == '')) {
- if (($sPhraseType == '' || !$bFirstPhrase) && !$bHasPartial) {
- $oSearch = clone $this;
- $oSearch->iSearchRank += 3 * $oSearchTerm->iTermCount;
- $oSearch->aAddress[$iWordID] = $iWordID;
- $aNewSearches[] = $oSearch;
- }
- } else {
- $oSearch = clone $this;
- $oSearch->iSearchRank++;
- $oSearch->aName = array($iWordID => $iWordID);
- if (CONST_Search_NameOnlySearchFrequencyThreshold) {
- $oSearch->bRareName =
- $oSearchTerm->iSearchNameCount
- < CONST_Search_NameOnlySearchFrequencyThreshold;
- }
- $aNewSearches[] = $oSearch;
- }
- }
+ /**
+ * Check if a postcode is currently included in the search.
+ *
+ * @return bool True, if a postcode is set.
+ */
+ public function hasPostcode()
+ {
+ return $this->sPostcode !== '';
+ }
- return $aNewSearches;
+ /**
+ * 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 !== '';
}
/**
- * Derive new searches by adding a partial term to the existing search.
+ * Check if a special type of place is requested.
*
- * @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 integer iOperator When set, check for the particular
+ * operator used for the special type.
*
- * @return SearchDescription[] List of derived search descriptions.
+ * @return bool True, if speial type is requested or, if requested,
+ * a special type with the given operator.
*/
- public function extendWithPartialTerm($sToken, $oSearchTerm, $bStructuredPhrases, $iPhrase, $aFullTokens)
+ public function hasOperator($iOperator = null)
{
- // Only allow name terms.
- if (!(is_a($oSearchTerm, '\Nominatim\Token\Word'))) {
- return array();
+ 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) {
+ $this->aAddress[$iId] = $iId;
+ } else {
+ $this->aAddressNonSearch[$iId] = $iId;
}
+ }
- $aNewSearches = array();
- $iWordID = $oSearchTerm->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->bNeedsAddress = false;
+ }
- if ((!$bStructuredPhrases || $iPhrase > 0)
- && (!empty($this->aName))
- && strpos($sToken, ' ') === false
- ) {
- if ($oSearchTerm->iSearchNameCount < CONST_Max_Word_Frequency) {
- $oSearch = clone $this;
- $oSearch->iSearchRank += $oSearchTerm->iTermCount + 1;
- if (empty($this->aName)) {
- $oSearch->iSearchRank++;
- }
- if (preg_match('#^[0-9]+$#', $sToken)) {
- $oSearch->iSearchRank++;
- }
- $oSearch->aAddress[$iWordID] = $iWordID;
- $aNewSearches[] = $oSearch;
- } else {
- $oSearch = clone $this;
- $oSearch->iSearchRank += $oSearchTerm->iTermCount + 1;
- $oSearch->aAddressNonSearch[$iWordID] = $iWordID;
- if (!empty($aFullTokens)) {
- $oSearch->iSearchRank++;
- }
- $aNewSearches[] = $oSearch;
-
- // revert to the token version?
- foreach ($aFullTokens as $oSearchTermToken) {
- if (is_a($oSearchTermToken, '\Nominatim\Token\Word')) {
- $oSearch = clone $this;
- $oSearch->iSearchRank += 3;
- $oSearch->aAddress[$oSearchTermToken->iId]
- = $oSearchTermToken->iId;
- $aNewSearches[] = $oSearch;
- }
- }
- }
+ /**
+ * 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 ((!$this->sPostcode && !$this->aAddress && !$this->aAddressNonSearch)
- && (empty($this->aName) || $this->iNamePhrase == $iPhrase)
- ) {
- $oSearch = clone $this;
- $oSearch->iSearchRank += 2;
- if (empty($this->aName)) {
- $oSearch->iSearchRank += 1;
- }
- if (preg_match('#^[0-9]+$#', $sToken)) {
- $oSearch->iSearchRank += 2;
- }
- if ($oSearchTerm->iSearchNameCount < CONST_Max_Word_Frequency) {
- if (empty($this->aName)
- && CONST_Search_NameOnlySearchFrequencyThreshold
- ) {
- $oSearch->bRareName =
- $oSearchTerm->iSearchNameCount
- < CONST_Search_NameOnlySearchFrequencyThreshold;
- } else {
- $oSearch->bRareName = false;
- }
- $oSearch->aName[$iWordID] = $iWordID;
- } else {
- $oSearch->aNameNonSearch[$iWordID] = $iWordID;
- }
- $oSearch->iNamePhrase = $iPhrase;
- $aNewSearches[] = $oSearch;
+ if ($bSearchable) {
+ $this->aName[$iId] = $iId;
+ } else {
+ $this->aNameNonSearch[$iId] = $iId;
}
+ $this->iNamePhrase = $iPhraseNumber;
+ }
- return $aNewSearches;
+ /**
+ * 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;
+ $this->aAddress = array_merge($this->aAddress, $this->aName);
+ $this->aName = array($iId => $sPostcode);
+ $this->bRareName = true;
+ $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->aName = array($iId => $iId);
+ $this->iNamePhrase = -1;
+ }
+
+ /**
+ * Make this search a POI search.
+ *
+ * In a POI search, objects are not (only) searched by their name
+ * but also by the primary OSM key/value pair (class and type in Nominatim).
+ *
+ * @param integer $iOperator Type of POI search
+ * @param string $sClass Class (or OSM tag key) of POI.
+ * @param string $sType Type (or OSM tag value) of POI.
+ *
+ * @return void
+ */
+ public function setPoiSearch($iOperator, $sClass, $sType)
+ {
+ $this->iOperator = $iOperator;
+ $this->sClass = $sClass;
+ $this->sType = $sType;
+ $this->iNamePhrase = -1;
+ }
+
+ public function getNamePhrase()
+ {
+ return $this->iNamePhrase;
+ }
+
+ /**
+ * Get the global search context.
+ *
+ * @return object Objects of global search constraints.
+ */
+ public function getContext()
+ {
+ return $this->oContext;
}
/////////// Query functions
public function query(&$oDB, $iMinRank, $iMaxRank, $iLimit)
{
$aResults = array();
- $iHousenumber = -1;
if ($this->sCountryCode
&& empty($this->aName)
// Now search for housenumber, if housenumber provided. Can be zero.
if (($this->sHouseNumber || $this->sHouseNumber === '0') && !empty($aResults)) {
- // Downgrade the rank of the street results, they are missing
- // the housenumber.
- foreach ($aResults as $oRes) {
- $oRes->iResultRank++;
- }
-
$aHnResults = $this->queryHouseNumber($oDB, $aResults);
- if (!empty($aHnResults)) {
- foreach ($aHnResults as $oRes) {
- $aResults[$oRes->iId] = $oRes;
+ // Downgrade the rank of the street results, they are missing
+ // 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 < 28) {
+ if ($oRes->iAddressRank >= 26) {
+ $oRes->iResultRank++;
+ } else {
+ $oRes->iResultRank += 2;
+ }
+ $aHnResults[$oRes->iId] = $oRes;
}
}
+
+ $aResults = $aHnResults;
}
// finally get POIs if requested
// 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
$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."')";
}
$aResults = array();
if (!empty($aTerms)) {
- $sSQL = 'SELECT place_id,'.$sExactMatchSQL;
+ $sSQL = 'SELECT place_id, address_rank,'.$sExactMatchSQL;
$sSQL .= ' FROM search_name';
$sSQL .= ' WHERE '.join(' and ', $aTerms);
$sSQL .= ' ORDER BY '.join(', ', $aOrder);
foreach ($aDBResults as $aResult) {
$oResult = new Result($aResult['place_id']);
$oResult->iExactMatches = $aResult['exactmatch'];
+ $oResult->iAddressRank = $aResult['address_rank'];
$aResults[$aResult['place_id']] = $oResult;
}
}
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);
$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.
}
}
- // 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 {