X-Git-Url: https://git.openstreetmap.org/nominatim.git/blobdiff_plain/5c7b9ef909b3c30bbe640f4373a1127e15a7e018..3127d59613c54c58a77784ece4c0e2de02d5a282:/lib-php/SearchDescription.php diff --git a/lib-php/SearchDescription.php b/lib-php/SearchDescription.php index 189ffa74..5d2caf00 100644 --- a/lib-php/SearchDescription.php +++ b/lib-php/SearchDescription.php @@ -1,4 +1,12 @@ 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. * @@ -142,257 +123,255 @@ 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; + $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; - $oSearch->iNamePhrase = -1; - } - $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++; - $oSearch->iNamePhrase = -1; - 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->iNamePhrase = -1; - $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++; - } - 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 += 2; - $oSearch->iNamePhrase = -1; - - $iOp = $oSearchTerm->iOperator; - if ($iOp == Operator::NONE) { - if (!empty($this->aName) || $this->oContext->isBoundedSearch()) { - $iOp = Operator::NAME; - } else { - $iOp = Operator::NEAR; - } - $oSearch->iSearchRank += 2; - } elseif (!$bFirstToken && !$bLastToken) { - $oSearch->iSearchRank += 2; - } - if ($this->sHouseNumber) { - $oSearch->iSearchRank++; - } + /** + * 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->iNamePhrase = -1; - $oSearch->iSearchRank += 3 * $oSearchTerm->iTermCount; - $oSearch->aAddress[$iWordID] = $iWordID; - $aNewSearches[] = $oSearch; - } - } elseif (empty($this->aNameNonSearch)) { - $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 integer 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; + } - if ((!$bStructuredPhrases || $iPhrase > 0) - && (!empty($this->aName)) - ) { - $oSearch = clone $this; - $oSearch->iSearchRank++; - if (preg_match('#^[0-9 ]+$#', $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; + /** + * 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; + } elseif ($bSearchable && count($this->aName) >= 2) { + $this->bNameNeedsAddress = false; + } else { + $this->bNameNeedsAddress &= $bNeedsAddress; } - - if ((!$this->sPostcode && !$this->aAddress && !$this->aAddressNonSearch) - && ((empty($this->aName) && empty($this->aNameNonSearch)) || $this->iNamePhrase == $iPhrase) - && strpos($sToken, ' ') === false - ) { - $oSearch = clone $this; - $oSearch->iSearchRank++; - if (empty($this->aName) && empty($this->aNameNonSearch)) { - $oSearch->iSearchRank++; - } - if (preg_match('#^[0-9 ]+$#', $sToken)) { - $oSearch->iSearchRank++; - } - 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 { - if (!empty($aFullTokens)) { - $oSearch->iSearchRank++; - } - $oSearch->aNameNonSearch[$iWordID] = $iWordID; - } - $oSearch->iNamePhrase = $iPhrase; - $aNewSearches[] = $oSearch; + if ($bSearchable) { + $this->aName[$iId] = $iId; + } else { + $this->aNameNonSearch[$iId] = $iId; } + $this->iNamePhrase = $iPhraseNumber; + } + + /** + * 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; + } - return $aNewSearches; + /** + * 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->bNameNeedsAddress = true; + $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 @@ -408,12 +387,11 @@ class SearchDescription * * @return mixed[] An array with two fields: IDs contains the list of * matching place IDs and houseNumber the houseNumber - * if appicable or -1 if not. + * if applicable or -1 if not. */ public function query(&$oDB, $iMinRank, $iMaxRank, $iLimit) { $aResults = array(); - $iHousenumber = -1; if ($this->sCountryCode && empty($this->aName) @@ -444,27 +422,6 @@ class SearchDescription $iLimit ); - // 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) { - if ($oRes->iAddressRank >= 26) { - $oRes->iResultRank++; - } else { - $oRes->iResultRank += 2; - } - } - - $aHnResults = $this->queryHouseNumber($oDB, $aResults); - - if (!empty($aHnResults)) { - foreach ($aHnResults as $oRes) { - $aResults[$oRes->iId] = $oRes; - } - } - } - // finally get POIs if requested if ($this->sClass && !empty($aResults)) { $aResults = $this->queryPoiByOperator($oDB, $aResults, $iLimit); @@ -610,36 +567,6 @@ class SearchDescription $aTerms = array(); $aOrder = array(); - // 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 - // 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] .= ')'; - } - $aOrder[0] .= ') DESC'; - } - if (!empty($this->aName)) { $aTerms[] = 'name_vector @> '.$oDB->getArraySQL($this->aName); } @@ -670,7 +597,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."')"; } @@ -685,10 +612,6 @@ class SearchDescription $aTerms[] = 'centroid && '.$this->oContext->sqlViewboxSmall; } - if ($this->oContext->hasNearPoint()) { - $aOrder[] = $this->oContext->distanceSQL('centroid'); - } - if ($this->sHouseNumber) { $sImportanceSQL = '- abs(26 - address_rank) + 3'; } else { @@ -711,105 +634,128 @@ class SearchDescription $sExactMatchSQL = '0::int as exactmatch'; } - if ($this->sHouseNumber || $this->sClass) { - $iLimit = 40; + if (empty($aTerms)) { + return array(); } - $aResults = array(); - - if (!empty($aTerms)) { - $sSQL = 'SELECT place_id, address_rank,'.$sExactMatchSQL; - $sSQL .= ' FROM search_name'; - $sSQL .= ' WHERE '.join(' and ', $aTerms); - $sSQL .= ' ORDER BY '.join(', ', $aOrder); - $sSQL .= ' LIMIT '.$iLimit; + if ($this->hasHousenumber()) { + $sHouseNumberRegex = $oDB->getDBQuoted('\\\\m'.$this->sHouseNumber.'\\\\M'); + + // Housenumbers on streets and places. + $sPlacexSql = 'SELECT array_agg(place_id) FROM placex'; + $sPlacexSql .= ' WHERE parent_place_id = sin.place_id AND sin.address_rank < 30'; + $sPlacexSql .= $this->oContext->excludeSQL(' AND place_id'); + $sPlacexSql .= ' and housenumber ~* E'.$sHouseNumberRegex; + + // Interpolations on streets and places. + $sInterpolSql = 'null'; + $sTigerSql = 'null'; + if (preg_match('/^[0-9]+$/', $this->sHouseNumber)) { + $sIpolHnr = 'WHERE parent_place_id = sin.place_id '; + $sIpolHnr .= ' AND startnumber is not NULL AND sin.address_rank < 30'; + $sIpolHnr .= ' AND '.$this->sHouseNumber.' between startnumber and endnumber'; + $sIpolHnr .= ' AND ('.$this->sHouseNumber.' - startnumber) % step = 0'; + + $sInterpolSql = 'SELECT array_agg(place_id) FROM location_property_osmline '.$sIpolHnr; + if (CONST_Use_US_Tiger_Data) { + $sTigerSql = 'SELECT array_agg(place_id) FROM location_property_tiger '.$sIpolHnr; + $sTigerSql .= " and sin.country_code = 'us'"; + } + } - Debug::printSQL($sSQL); + if ($this->sClass) { + $iLimit = 40; + } - $aDBResults = $oDB->getAll($sSQL, null, 'Could not get places for search terms.'); + $sSelfHnr = 'SELECT * FROM placex WHERE place_id = search_name.place_id'; + $sSelfHnr .= ' AND housenumber ~* E'.$sHouseNumberRegex; - foreach ($aDBResults as $aResult) { - $oResult = new Result($aResult['place_id']); - $oResult->iExactMatches = $aResult['exactmatch']; - $oResult->iAddressRank = $aResult['address_rank']; - $aResults[$aResult['place_id']] = $oResult; - } - } + $aTerms[] = '(address_rank < 30 or exists('.$sSelfHnr.'))'; - return $aResults; - } - private function queryHouseNumber(&$oDB, $aRoadPlaceIDs) - { - $aResults = array(); - $sPlaceIDs = Result::joinIdsByTable($aRoadPlaceIDs, Result::TABLE_PLACEX); + $sSQL = 'SELECT sin.*, '; + $sSQL .= '('.$sPlacexSql.') as placex_hnr, '; + $sSQL .= '('.$sInterpolSql.') as interpol_hnr, '; + $sSQL .= '('.$sTigerSql.') as tiger_hnr '; + $sSQL .= ' FROM ('; + $sSQL .= ' SELECT place_id, address_rank, country_code,'.$sExactMatchSQL.','; + $sSQL .= ' CASE WHEN importance = 0 OR importance IS NULL'; + $sSQL .= ' THEN 0.75001-(search_rank::float/40) ELSE importance END as importance'; + $sSQL .= ' FROM search_name'; + $sSQL .= ' WHERE '.join(' and ', $aTerms); + $sSQL .= ' ORDER BY '.join(', ', $aOrder); + $sSQL .= ' LIMIT 40000'; + $sSQL .= ') as sin'; + $sSQL .= ' ORDER BY address_rank = 30 desc, placex_hnr, interpol_hnr, tiger_hnr,'; + $sSQL .= ' importance'; + $sSQL .= ' LIMIT '.$iLimit; + } else { + if ($this->sClass) { + $iLimit = 40; + } - if (!$sPlaceIDs) { - return $aResults; + $sSQL = 'SELECT place_id, address_rank, '.$sExactMatchSQL; + $sSQL .= ' FROM search_name'; + $sSQL .= ' WHERE '.join(' and ', $aTerms); + $sSQL .= ' ORDER BY '.join(', ', $aOrder); + $sSQL .= ' LIMIT '.$iLimit; } - $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M'; - $sSQL = 'SELECT place_id FROM placex '; - $sSQL .= 'WHERE parent_place_id in ('.$sPlaceIDs.')'; - $sSQL .= " AND housenumber ~* E'".$sHouseNumberRegex."'"; - $sSQL .= $this->oContext->excludeSQL(' AND place_id'); - Debug::printSQL($sSQL); - // XXX should inherit the exactMatches from its parent - foreach ($oDB->getCol($sSQL) as $iPlaceId) { - $aResults[$iPlaceId] = new Result($iPlaceId); - } + $aDBResults = $oDB->getAll($sSQL, null, 'Could not get places for search terms.'); - $bIsIntHouseNumber= (bool) preg_match('/[0-9]+/', $this->sHouseNumber); - $iHousenumber = intval($this->sHouseNumber); - if ($bIsIntHouseNumber && 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 ('; - if ($iHousenumber % 2 == 0) { - // If housenumber is even, look for housenumber in streets - // with interpolationtype even or all. - $sSQL .= "interpolationtype='even'"; - } else { - // Else look for housenumber with interpolationtype odd or all. - $sSQL .= "interpolationtype='odd'"; - } - $sSQL .= " or interpolationtype='all') and "; - $sSQL .= $iHousenumber.'>=startnumber and '; - $sSQL .= $iHousenumber.'<=endnumber'; - $sSQL .= $this->oContext->excludeSQL(' AND place_id'); - - Debug::printSQL($sSQL); + $aResults = array(); - foreach ($oDB->getCol($sSQL) as $iPlaceId) { - $oResult = new Result($iPlaceId, Result::TABLE_OSMLINE); - $oResult->iHouseNumber = $iHousenumber; - $aResults[$iPlaceId] = $oResult; - } - } + foreach ($aDBResults as $aResult) { + $oResult = new Result($aResult['place_id']); + $oResult->iExactMatches = $aResult['exactmatch']; + $oResult->iAddressRank = $aResult['address_rank']; + + $bNeedResult = true; + if ($this->hasHousenumber() && $aResult['address_rank'] < 30) { + if ($aResult['placex_hnr']) { + foreach (explode(',', substr($aResult['placex_hnr'], 1, -1)) as $sPlaceID) { + $iPlaceID = intval($sPlaceID); + $oHnrResult = new Result($iPlaceID); + $oHnrResult->iExactMatches = $aResult['exactmatch']; + $oHnrResult->iAddressRank = 30; + $aResults[$iPlaceID] = $oHnrResult; + $bNeedResult = false; + } + } + if ($aResult['interpol_hnr']) { + foreach (explode(',', substr($aResult['interpol_hnr'], 1, -1)) as $sPlaceID) { + $iPlaceID = intval($sPlaceID); + $oHnrResult = new Result($iPlaceID, Result::TABLE_OSMLINE); + $oHnrResult->iExactMatches = $aResult['exactmatch']; + $oHnrResult->iAddressRank = 30; + $oHnrResult->iHouseNumber = intval($this->sHouseNumber); + $aResults[$iPlaceID] = $oHnrResult; + $bNeedResult = false; + } + } + if ($aResult['tiger_hnr']) { + foreach (explode(',', substr($aResult['tiger_hnr'], 1, -1)) as $sPlaceID) { + $iPlaceID = intval($sPlaceID); + $oHnrResult = new Result($iPlaceID, Result::TABLE_TIGER); + $oHnrResult->iExactMatches = $aResult['exactmatch']; + $oHnrResult->iAddressRank = 30; + $oHnrResult->iHouseNumber = intval($this->sHouseNumber); + $aResults[$iPlaceID] = $oHnrResult; + $bNeedResult = false; + } + } - // If nothing found then search in Tiger data (location_property_tiger) - if (CONST_Use_US_Tiger_Data && $bIsIntHouseNumber && empty($aResults)) { - $sSQL = 'SELECT place_id FROM location_property_tiger'; - $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.') and ('; - if ($iHousenumber % 2 == 0) { - $sSQL .= "interpolationtype='even'"; - } else { - $sSQL .= "interpolationtype='odd'"; + if ($aResult['address_rank'] < 26) { + $oResult->iResultRank += 2; + } else { + $oResult->iResultRank++; + } } - $sSQL .= " or interpolationtype='all') and "; - $sSQL .= $iHousenumber.'>=startnumber and '; - $sSQL .= $iHousenumber.'<=endnumber'; - $sSQL .= $this->oContext->excludeSQL(' AND place_id'); - Debug::printSQL($sSQL); - - foreach ($oDB->getCol($sSQL) as $iPlaceId) { - $oResult = new Result($iPlaceId, Result::TABLE_TIGER); - $oResult->iHouseNumber = $iHousenumber; - $aResults[$iPlaceId] = $oResult; + if ($bNeedResult) { + $aResults[$aResult['place_id']] = $oResult; } } @@ -861,6 +807,7 @@ class SearchDescription $sSQL = 'SELECT geometry FROM placex'; $sSQL .= " WHERE place_id in ($sPlaceIDs)"; $sSQL .= " AND rank_search < $iMaxRank + 5"; + $sSQL .= ' AND ST_Area(Box2d(geometry)) < 20'; $sSQL .= " AND ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon')"; $sSQL .= ' ORDER BY rank_search ASC '; $sSQL .= ' LIMIT 1';