5 require_once(CONST_LibDir.'/SpecialSearchOperator.php');
6 require_once(CONST_LibDir.'/SearchContext.php');
7 require_once(CONST_LibDir.'/Result.php');
10 * Description of a single interpretation of a search query.
12 class SearchDescription
14 /// Ranking how well the description fits the query.
15 private $iSearchRank = 0;
16 /// Country code of country the result must belong to.
17 private $sCountryCode = '';
18 /// List of word ids making up the name of the object.
19 private $aName = array();
20 /// True if the name is rare enough to force index use on name.
21 private $bRareName = false;
22 /// List of word ids making up the address of the object.
23 private $aAddress = array();
24 /// List of word ids that appear in the name but should be ignored.
25 private $aNameNonSearch = array();
26 /// List of word ids that appear in the address but should be ignored.
27 private $aAddressNonSearch = array();
28 /// Kind of search for special searches, see Nominatim::Operator.
29 private $iOperator = Operator::NONE;
30 /// Class of special feature to search for.
32 /// Type of special feature to search for.
34 /// Housenumber of the object.
35 private $sHouseNumber = '';
36 /// Postcode for the object.
37 private $sPostcode = '';
38 /// Global search constraints.
41 // Temporary values used while creating the search description.
43 /// Index of phrase currently processed.
44 private $iNamePhrase = -1;
47 * Create an empty search description.
49 * @param object $oContext Global context to use. Will be inherited by
50 * all derived search objects.
52 public function __construct($oContext)
54 $this->oContext = $oContext;
58 * Get current search rank.
60 * The higher the search rank the lower the likelihood that the
61 * search is a correct interpretation of the search query.
63 * @return integer Search rank.
65 public function getRank()
67 return $this->iSearchRank;
71 * Extract key/value pairs from a query.
73 * Key/value pairs are recognised if they are of the form [<key>=<value>].
74 * If multiple terms of this kind are found then all terms are removed
75 * but only the first is used for search.
77 * @param string $sQuery Original query string.
79 * @return string The query string with the special search patterns removed.
81 public function extractKeyValuePairs($sQuery)
83 // Search for terms of kind [<key>=<value>].
85 '/\\[([\\w_]*)=([\\w_]*)\\]/',
91 foreach ($aSpecialTermsRaw as $aTerm) {
92 $sQuery = str_replace($aTerm[0], ' ', $sQuery);
93 if (!$this->hasOperator()) {
94 $this->setPoiSearch(Operator::TYPE, $aTerm[1], $aTerm[2]);
102 * Check if the combination of parameters is sensible.
104 * @return bool True, if the search looks valid.
106 public function isValidSearch()
108 if (empty($this->aName)) {
109 if ($this->sHouseNumber) {
112 if (!$this->sClass && !$this->sCountryCode) {
120 /////////// Search building functions
121 public function clone($iTermCost)
123 $oSearch = clone $this;
124 $oSearch->iSearchRank += $iTermCost;
129 public function hasName($bIncludeNonNames = false)
131 return !empty($this->aName)
132 || (!empty($this->aNameNonSearch) && $bIncludeNonNames);
135 public function hasAddress()
137 return !empty($this->aAddress) || !empty($this->aAddressNonSearch);
140 public function hasCountry()
142 return $this->sCountryCode !== '';
145 public function hasPostcode()
147 return $this->sPostcode !== '';
150 public function hasHousenumber()
152 return $this->sHouseNumber !== '';
155 public function hasOperator($iOperator = null)
157 return $iOperator === null ? $this->iOperator != Operator::NONE : $this->iOperator == $iOperator;
160 public function addAddressToken($iId, $bSearchable = true)
163 $this->aAddress[$iId] = $iId;
165 $this->aAddressNonSearch[$iId] = $iId;
169 public function addNameToken($iId)
171 $this->aName[$iId] = $iId;
174 public function addPartialNameToken($iId, $bSearchable, $iPhraseNumber)
177 $this->aName[$iId] = $iId;
179 $this->aNameNonSearch[$iId] = $iId;
181 $this->iNamePhrase = $iPhraseNumber;
184 public function markRareName()
186 $this->bRareName = true;
189 public function setCountry($sCountryCode)
191 $this->sCountryCode = $sCountryCode;
192 $this->iNamePhrase = -1;
195 public function setPostcode($sPostcode)
197 $this->sPostcode = $sPostcode;
198 $this->iNamePhrase = -1;
201 public function setPostcodeAsName($iId, $sPostcode)
203 $this->iOperator = Operator::POSTCODE;
204 $this->aAddress = array_merge($this->aAddress, $this->aName);
205 $this->aName = array($iId => $sPostcode);
206 $this->bRareName = true;
207 $this->iNamePhrase = -1;
210 public function setHousenumber($sNumber)
212 $this->sHouseNumber = $sNumber;
213 $this->iNamePhrase = -1;
216 public function setHousenumberAsName($iId)
218 $this->aAddress = array_merge($this->aAddress, $this->aName);
219 $this->bRareName = false;
220 $this->aName = array($iId => $iId);
221 $this->iNamePhrase = -1;
225 * Make this search a POI search.
227 * In a POI search, objects are not (only) searched by their name
228 * but also by the primary OSM key/value pair (class and type in Nominatim).
230 * @param integer $iOperator Type of POI search
231 * @param string $sClass Class (or OSM tag key) of POI.
232 * @param string $sType Type (or OSM tag value) of POI.
236 public function setPoiSearch($iOperator, $sClass, $sType)
238 $this->iOperator = $iOperator;
239 $this->sClass = $sClass;
240 $this->sType = $sType;
241 $this->iNamePhrase = -1;
244 public function getNamePhrase()
246 return $this->iNamePhrase;
249 public function getContext()
251 return $this->oContext;
254 /////////// Query functions
258 * Query database for places that match this search.
260 * @param object $oDB Nominatim::DB instance to use.
261 * @param integer $iMinRank Minimum address rank to restrict search to.
262 * @param integer $iMaxRank Maximum address rank to restrict search to.
263 * @param integer $iLimit Maximum number of results.
265 * @return mixed[] An array with two fields: IDs contains the list of
266 * matching place IDs and houseNumber the houseNumber
267 * if appicable or -1 if not.
269 public function query(&$oDB, $iMinRank, $iMaxRank, $iLimit)
273 if ($this->sCountryCode
274 && empty($this->aName)
277 && !$this->oContext->hasNearPoint()
279 // Just looking for a country - look it up
280 if (4 >= $iMinRank && 4 <= $iMaxRank) {
281 $aResults = $this->queryCountry($oDB);
283 } elseif (empty($this->aName) && empty($this->aAddress)) {
284 // Neither name nor address? Then we must be
285 // looking for a POI in a geographic area.
286 if ($this->oContext->isBoundedSearch()) {
287 $aResults = $this->queryNearbyPoi($oDB, $iLimit);
289 } elseif ($this->iOperator == Operator::POSTCODE) {
290 // looking for postcode
291 $aResults = $this->queryPostcode($oDB, $iLimit);
294 // First search for places according to name and address.
295 $aResults = $this->queryNamedPlace(
302 // Now search for housenumber, if housenumber provided. Can be zero.
303 if (($this->sHouseNumber || $this->sHouseNumber === '0') && !empty($aResults)) {
304 $aHnResults = $this->queryHouseNumber($oDB, $aResults);
306 // Downgrade the rank of the street results, they are missing
307 // the housenumber. Also drop POI places (rank 30) here, they
308 // cannot be a parent place and therefore must not be shown
309 // as a result for a search with a missing housenumber.
310 foreach ($aResults as $oRes) {
311 if ($oRes->iAddressRank < 28) {
312 if ($oRes->iAddressRank >= 26) {
313 $oRes->iResultRank++;
315 $oRes->iResultRank += 2;
317 $aHnResults[$oRes->iId] = $oRes;
321 $aResults = $aHnResults;
324 // finally get POIs if requested
325 if ($this->sClass && !empty($aResults)) {
326 $aResults = $this->queryPoiByOperator($oDB, $aResults, $iLimit);
330 Debug::printDebugTable('Place IDs', $aResults);
332 if (!empty($aResults) && $this->sPostcode) {
333 $sPlaceIds = Result::joinIdsByTable($aResults, Result::TABLE_PLACEX);
335 $sSQL = 'SELECT place_id FROM placex';
336 $sSQL .= ' WHERE place_id in ('.$sPlaceIds.')';
337 $sSQL .= " AND postcode != '".$this->sPostcode."'";
338 Debug::printSQL($sSQL);
339 $aFilteredPlaceIDs = $oDB->getCol($sSQL);
340 if ($aFilteredPlaceIDs) {
341 foreach ($aFilteredPlaceIDs as $iPlaceId) {
342 $aResults[$iPlaceId]->iResultRank++;
352 private function queryCountry(&$oDB)
354 $sSQL = 'SELECT place_id FROM placex ';
355 $sSQL .= "WHERE country_code='".$this->sCountryCode."'";
356 $sSQL .= ' AND rank_search = 4';
357 if ($this->oContext->bViewboxBounded) {
358 $sSQL .= ' AND ST_Intersects('.$this->oContext->sqlViewboxSmall.', geometry)';
360 $sSQL .= ' ORDER BY st_area(geometry) DESC LIMIT 1';
362 Debug::printSQL($sSQL);
364 $iPlaceId = $oDB->getOne($sSQL);
368 $aResults[$iPlaceId] = new Result($iPlaceId);
374 private function queryNearbyPoi(&$oDB, $iLimit)
376 if (!$this->sClass) {
380 $aDBResults = array();
381 $sPoiTable = $this->poiTable();
383 if ($oDB->tableExists($sPoiTable)) {
384 $sSQL = 'SELECT place_id FROM '.$sPoiTable.' ct';
385 if ($this->oContext->sqlCountryList) {
386 $sSQL .= ' JOIN placex USING (place_id)';
388 if ($this->oContext->hasNearPoint()) {
389 $sSQL .= ' WHERE '.$this->oContext->withinSQL('ct.centroid');
390 } elseif ($this->oContext->bViewboxBounded) {
391 $sSQL .= ' WHERE ST_Contains('.$this->oContext->sqlViewboxSmall.', ct.centroid)';
393 if ($this->oContext->sqlCountryList) {
394 $sSQL .= ' AND country_code in '.$this->oContext->sqlCountryList;
396 $sSQL .= $this->oContext->excludeSQL(' AND place_id');
397 if ($this->oContext->sqlViewboxCentre) {
398 $sSQL .= ' ORDER BY ST_Distance(';
399 $sSQL .= $this->oContext->sqlViewboxCentre.', ct.centroid) ASC';
400 } elseif ($this->oContext->hasNearPoint()) {
401 $sSQL .= ' ORDER BY '.$this->oContext->distanceSQL('ct.centroid').' ASC';
403 $sSQL .= " LIMIT $iLimit";
404 Debug::printSQL($sSQL);
405 $aDBResults = $oDB->getCol($sSQL);
408 if ($this->oContext->hasNearPoint()) {
409 $sSQL = 'SELECT place_id FROM placex WHERE ';
410 $sSQL .= 'class = :class and type = :type';
411 $sSQL .= ' AND '.$this->oContext->withinSQL('geometry');
412 $sSQL .= ' AND linked_place_id is null';
413 if ($this->oContext->sqlCountryList) {
414 $sSQL .= ' AND country_code in '.$this->oContext->sqlCountryList;
416 $sSQL .= ' ORDER BY '.$this->oContext->distanceSQL('centroid').' ASC';
417 $sSQL .= " LIMIT $iLimit";
418 Debug::printSQL($sSQL);
419 $aDBResults = $oDB->getCol(
421 array(':class' => $this->sClass, ':type' => $this->sType)
426 foreach ($aDBResults as $iPlaceId) {
427 $aResults[$iPlaceId] = new Result($iPlaceId);
433 private function queryPostcode(&$oDB, $iLimit)
435 $sSQL = 'SELECT p.place_id FROM location_postcode p ';
437 if (!empty($this->aAddress)) {
438 $sSQL .= ', search_name s ';
439 $sSQL .= 'WHERE s.place_id = p.parent_place_id ';
440 $sSQL .= 'AND array_cat(s.nameaddress_vector, s.name_vector)';
441 $sSQL .= ' @> '.$oDB->getArraySQL($this->aAddress).' AND ';
446 $sSQL .= "p.postcode = '".reset($this->aName)."'";
447 $sSQL .= $this->countryCodeSQL(' AND p.country_code');
448 if ($this->oContext->bViewboxBounded) {
449 $sSQL .= ' AND ST_Intersects('.$this->oContext->sqlViewboxSmall.', geometry)';
451 $sSQL .= $this->oContext->excludeSQL(' AND p.place_id');
452 $sSQL .= " LIMIT $iLimit";
454 Debug::printSQL($sSQL);
457 foreach ($oDB->getCol($sSQL) as $iPlaceId) {
458 $aResults[$iPlaceId] = new Result($iPlaceId, Result::TABLE_POSTCODE);
464 private function queryNamedPlace(&$oDB, $iMinAddressRank, $iMaxAddressRank, $iLimit)
469 // Sort by existence of the requested house number but only if not
470 // too many results are expected for the street, i.e. if the result
471 // will be narrowed down by an address. Remeber that with ordering
472 // every single result has to be checked.
473 if ($this->sHouseNumber && ($this->bRareName || !empty($this->aAddress) || $this->sPostcode)) {
474 $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
476 $aOrder[0] .= 'EXISTS(';
477 $aOrder[0] .= ' SELECT place_id';
478 $aOrder[0] .= ' FROM placex';
479 $aOrder[0] .= ' WHERE parent_place_id = search_name.place_id';
480 $aOrder[0] .= " AND housenumber ~* E'".$sHouseNumberRegex."'";
481 $aOrder[0] .= ' LIMIT 1';
483 // also housenumbers from interpolation lines table are needed
484 if (preg_match('/[0-9]+/', $this->sHouseNumber)) {
485 $iHouseNumber = intval($this->sHouseNumber);
486 $aOrder[0] .= 'OR EXISTS(';
487 $aOrder[0] .= ' SELECT place_id ';
488 $aOrder[0] .= ' FROM location_property_osmline ';
489 $aOrder[0] .= ' WHERE parent_place_id = search_name.place_id';
490 $aOrder[0] .= ' AND startnumber is not NULL';
491 $aOrder[0] .= ' AND '.$iHouseNumber.'>=startnumber ';
492 $aOrder[0] .= ' AND '.$iHouseNumber.'<=endnumber ';
493 $aOrder[0] .= ' LIMIT 1';
496 $aOrder[0] .= ') DESC';
499 if (!empty($this->aName)) {
500 $aTerms[] = 'name_vector @> '.$oDB->getArraySQL($this->aName);
502 if (!empty($this->aAddress)) {
503 // For infrequent name terms disable index usage for address
504 if ($this->bRareName) {
505 $aTerms[] = 'array_cat(nameaddress_vector,ARRAY[]::integer[]) @> '.$oDB->getArraySQL($this->aAddress);
507 $aTerms[] = 'nameaddress_vector @> '.$oDB->getArraySQL($this->aAddress);
511 $sCountryTerm = $this->countryCodeSQL('country_code');
513 $aTerms[] = $sCountryTerm;
516 if ($this->sHouseNumber) {
517 $aTerms[] = 'address_rank between 16 and 30';
518 } elseif (!$this->sClass || $this->iOperator == Operator::NAME) {
519 if ($iMinAddressRank > 0) {
520 $aTerms[] = "((address_rank between $iMinAddressRank and $iMaxAddressRank) or (search_rank between $iMinAddressRank and $iMaxAddressRank))";
524 if ($this->oContext->hasNearPoint()) {
525 $aTerms[] = $this->oContext->withinSQL('centroid');
526 $aOrder[] = $this->oContext->distanceSQL('centroid');
527 } elseif ($this->sPostcode) {
528 if (empty($this->aAddress)) {
529 $aTerms[] = "EXISTS(SELECT place_id FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."' AND ST_DWithin(search_name.centroid, p.geometry, 0.1))";
531 $aOrder[] = "(SELECT min(ST_Distance(search_name.centroid, p.geometry)) FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."')";
535 $sExcludeSQL = $this->oContext->excludeSQL('place_id');
537 $aTerms[] = $sExcludeSQL;
540 if ($this->oContext->bViewboxBounded) {
541 $aTerms[] = 'centroid && '.$this->oContext->sqlViewboxSmall;
544 if ($this->oContext->hasNearPoint()) {
545 $aOrder[] = $this->oContext->distanceSQL('centroid');
548 if ($this->sHouseNumber) {
549 $sImportanceSQL = '- abs(26 - address_rank) + 3';
551 $sImportanceSQL = '(CASE WHEN importance = 0 OR importance IS NULL THEN 0.75001-(search_rank::float/40) ELSE importance END)';
553 $sImportanceSQL .= $this->oContext->viewboxImportanceSQL('centroid');
554 $aOrder[] = "$sImportanceSQL DESC";
556 $aFullNameAddress = $this->oContext->getFullNameTerms();
557 if (!empty($aFullNameAddress)) {
558 $sExactMatchSQL = ' ( ';
559 $sExactMatchSQL .= ' SELECT count(*) FROM ( ';
560 $sExactMatchSQL .= ' SELECT unnest('.$oDB->getArraySQL($aFullNameAddress).')';
561 $sExactMatchSQL .= ' INTERSECT ';
562 $sExactMatchSQL .= ' SELECT unnest(nameaddress_vector)';
563 $sExactMatchSQL .= ' ) s';
564 $sExactMatchSQL .= ') as exactmatch';
565 $aOrder[] = 'exactmatch DESC';
567 $sExactMatchSQL = '0::int as exactmatch';
570 if ($this->sHouseNumber || $this->sClass) {
576 if (!empty($aTerms)) {
577 $sSQL = 'SELECT place_id, address_rank,'.$sExactMatchSQL;
578 $sSQL .= ' FROM search_name';
579 $sSQL .= ' WHERE '.join(' and ', $aTerms);
580 $sSQL .= ' ORDER BY '.join(', ', $aOrder);
581 $sSQL .= ' LIMIT '.$iLimit;
583 Debug::printSQL($sSQL);
585 $aDBResults = $oDB->getAll($sSQL, null, 'Could not get places for search terms.');
587 foreach ($aDBResults as $aResult) {
588 $oResult = new Result($aResult['place_id']);
589 $oResult->iExactMatches = $aResult['exactmatch'];
590 $oResult->iAddressRank = $aResult['address_rank'];
591 $aResults[$aResult['place_id']] = $oResult;
598 private function queryHouseNumber(&$oDB, $aRoadPlaceIDs)
601 $sRoadPlaceIDs = Result::joinIdsByTableMaxRank(
603 Result::TABLE_PLACEX,
606 $sPOIPlaceIDs = Result::joinIdsByTableMinRank(
608 Result::TABLE_PLACEX,
612 $aIDCondition = array();
613 if ($sRoadPlaceIDs) {
614 $aIDCondition[] = 'parent_place_id in ('.$sRoadPlaceIDs.')';
617 $aIDCondition[] = 'place_id in ('.$sPOIPlaceIDs.')';
620 if (empty($aIDCondition)) {
624 $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
625 $sSQL = 'SELECT place_id FROM placex WHERE';
626 $sSQL .= " housenumber ~* E'".$sHouseNumberRegex."'";
627 $sSQL .= ' AND ('.join(' OR ', $aIDCondition).')';
628 $sSQL .= $this->oContext->excludeSQL(' AND place_id');
630 Debug::printSQL($sSQL);
632 // XXX should inherit the exactMatches from its parent
633 foreach ($oDB->getCol($sSQL) as $iPlaceId) {
634 $aResults[$iPlaceId] = new Result($iPlaceId);
637 $bIsIntHouseNumber= (bool) preg_match('/[0-9]+/', $this->sHouseNumber);
638 $iHousenumber = intval($this->sHouseNumber);
639 if ($bIsIntHouseNumber && $sRoadPlaceIDs && empty($aResults)) {
640 // if nothing found, search in the interpolation line table
641 $sSQL = 'SELECT distinct place_id FROM location_property_osmline';
642 $sSQL .= ' WHERE startnumber is not NULL';
643 $sSQL .= ' AND parent_place_id in ('.$sRoadPlaceIDs.') AND (';
644 if ($iHousenumber % 2 == 0) {
645 // If housenumber is even, look for housenumber in streets
646 // with interpolationtype even or all.
647 $sSQL .= "interpolationtype='even'";
649 // Else look for housenumber with interpolationtype odd or all.
650 $sSQL .= "interpolationtype='odd'";
652 $sSQL .= " or interpolationtype='all') and ";
653 $sSQL .= $iHousenumber.'>=startnumber and ';
654 $sSQL .= $iHousenumber.'<=endnumber';
655 $sSQL .= $this->oContext->excludeSQL(' AND place_id');
657 Debug::printSQL($sSQL);
659 foreach ($oDB->getCol($sSQL) as $iPlaceId) {
660 $oResult = new Result($iPlaceId, Result::TABLE_OSMLINE);
661 $oResult->iHouseNumber = $iHousenumber;
662 $aResults[$iPlaceId] = $oResult;
666 // If nothing found then search in Tiger data (location_property_tiger)
667 if (CONST_Use_US_Tiger_Data && $sRoadPlaceIDs && $bIsIntHouseNumber && empty($aResults)) {
668 $sSQL = 'SELECT place_id FROM location_property_tiger';
669 $sSQL .= ' WHERE parent_place_id in ('.$sRoadPlaceIDs.') and (';
670 if ($iHousenumber % 2 == 0) {
671 $sSQL .= "interpolationtype='even'";
673 $sSQL .= "interpolationtype='odd'";
675 $sSQL .= " or interpolationtype='all') and ";
676 $sSQL .= $iHousenumber.'>=startnumber and ';
677 $sSQL .= $iHousenumber.'<=endnumber';
678 $sSQL .= $this->oContext->excludeSQL(' AND place_id');
680 Debug::printSQL($sSQL);
682 foreach ($oDB->getCol($sSQL) as $iPlaceId) {
683 $oResult = new Result($iPlaceId, Result::TABLE_TIGER);
684 $oResult->iHouseNumber = $iHousenumber;
685 $aResults[$iPlaceId] = $oResult;
693 private function queryPoiByOperator(&$oDB, $aParentIDs, $iLimit)
696 $sPlaceIDs = Result::joinIdsByTable($aParentIDs, Result::TABLE_PLACEX);
702 if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NAME) {
703 // If they were searching for a named class (i.e. 'Kings Head pub')
704 // then we might have an extra match
705 $sSQL = 'SELECT place_id FROM placex ';
706 $sSQL .= " WHERE place_id in ($sPlaceIDs)";
707 $sSQL .= " AND class='".$this->sClass."' ";
708 $sSQL .= " AND type='".$this->sType."'";
709 $sSQL .= ' AND linked_place_id is null';
710 $sSQL .= $this->oContext->excludeSQL(' AND place_id');
711 $sSQL .= ' ORDER BY rank_search ASC ';
712 $sSQL .= " LIMIT $iLimit";
714 Debug::printSQL($sSQL);
716 foreach ($oDB->getCol($sSQL) as $iPlaceId) {
717 $aResults[$iPlaceId] = new Result($iPlaceId);
721 // NEAR and IN are handled the same
722 if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NEAR) {
723 $sClassTable = $this->poiTable();
724 $bCacheTable = $oDB->tableExists($sClassTable);
726 $sSQL = "SELECT min(rank_search) FROM placex WHERE place_id in ($sPlaceIDs)";
727 Debug::printSQL($sSQL);
728 $iMaxRank = (int) $oDB->getOne($sSQL);
730 // For state / country level searches the normal radius search doesn't work very well
732 if ($iMaxRank < 9 && $bCacheTable) {
733 // Try and get a polygon to search in instead
734 $sSQL = 'SELECT geometry FROM placex';
735 $sSQL .= " WHERE place_id in ($sPlaceIDs)";
736 $sSQL .= " AND rank_search < $iMaxRank + 5";
737 $sSQL .= " AND ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon')";
738 $sSQL .= ' ORDER BY rank_search ASC ';
740 Debug::printSQL($sSQL);
741 $sPlaceGeom = $oDB->getOne($sSQL);
748 $sSQL = 'SELECT place_id FROM placex';
749 $sSQL .= " WHERE place_id in ($sPlaceIDs) and rank_search < $iMaxRank";
750 Debug::printSQL($sSQL);
751 $aPlaceIDs = $oDB->getCol($sSQL);
752 $sPlaceIDs = join(',', $aPlaceIDs);
755 if ($sPlaceIDs || $sPlaceGeom) {
758 // More efficient - can make the range bigger
762 if ($this->oContext->hasNearPoint()) {
763 $sOrderBySQL = $this->oContext->distanceSQL('l.centroid');
764 } elseif ($sPlaceIDs) {
765 $sOrderBySQL = 'ST_Distance(l.centroid, f.geometry)';
766 } elseif ($sPlaceGeom) {
767 $sOrderBySQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
770 $sSQL = 'SELECT distinct i.place_id';
772 $sSQL .= ', i.order_term';
774 $sSQL .= ' from (SELECT l.place_id';
776 $sSQL .= ','.$sOrderBySQL.' as order_term';
778 $sSQL .= ' from '.$sClassTable.' as l';
781 $sSQL .= ',placex as f WHERE ';
782 $sSQL .= "f.place_id in ($sPlaceIDs) ";
783 $sSQL .= " AND ST_DWithin(l.centroid, f.centroid, $fRange)";
784 } elseif ($sPlaceGeom) {
785 $sSQL .= " WHERE ST_Contains('$sPlaceGeom', l.centroid)";
788 $sSQL .= $this->oContext->excludeSQL(' AND l.place_id');
789 $sSQL .= 'limit 300) i ';
791 $sSQL .= 'order by order_term asc';
793 $sSQL .= " limit $iLimit";
795 Debug::printSQL($sSQL);
797 foreach ($oDB->getCol($sSQL) as $iPlaceId) {
798 $aResults[$iPlaceId] = new Result($iPlaceId);
801 if ($this->oContext->hasNearPoint()) {
802 $fRange = $this->oContext->nearRadius();
806 if ($this->oContext->hasNearPoint()) {
807 $sOrderBySQL = $this->oContext->distanceSQL('l.geometry');
809 $sOrderBySQL = 'ST_Distance(l.geometry, f.geometry)';
812 $sSQL = 'SELECT distinct l.place_id';
814 $sSQL .= ','.$sOrderBySQL.' as orderterm';
816 $sSQL .= ' FROM placex as l, placex as f';
817 $sSQL .= " WHERE f.place_id in ($sPlaceIDs)";
818 $sSQL .= " AND ST_DWithin(l.geometry, f.centroid, $fRange)";
819 $sSQL .= " AND l.class='".$this->sClass."'";
820 $sSQL .= " AND l.type='".$this->sType."'";
821 $sSQL .= $this->oContext->excludeSQL(' AND l.place_id');
823 $sSQL .= 'ORDER BY orderterm ASC';
825 $sSQL .= " limit $iLimit";
827 Debug::printSQL($sSQL);
829 foreach ($oDB->getCol($sSQL) as $iPlaceId) {
830 $aResults[$iPlaceId] = new Result($iPlaceId);
839 private function poiTable()
841 return 'place_classtype_'.$this->sClass.'_'.$this->sType;
844 private function countryCodeSQL($sVar)
846 if ($this->sCountryCode) {
847 return $sVar.' = \''.$this->sCountryCode."'";
849 if ($this->oContext->sqlCountryList) {
850 return $sVar.' in '.$this->oContext->sqlCountryList;
856 /////////// Sort functions
859 public static function bySearchRank($a, $b)
861 if ($a->iSearchRank == $b->iSearchRank) {
862 return $a->iOperator + strlen($a->sHouseNumber)
863 - $b->iOperator - strlen($b->sHouseNumber);
866 return $a->iSearchRank < $b->iSearchRank ? -1 : 1;
869 //////////// Debugging functions
872 public function debugInfo()
875 'Search rank' => $this->iSearchRank,
876 'Country code' => $this->sCountryCode,
877 'Name terms' => $this->aName,
878 'Name terms (stop words)' => $this->aNameNonSearch,
879 'Address terms' => $this->aAddress,
880 'Address terms (stop words)' => $this->aAddressNonSearch,
881 'Address terms (full words)' => $this->aFullNameAddress ?? '',
882 'Special search' => $this->iOperator,
883 'Class' => $this->sClass,
884 'Type' => $this->sType,
885 'House number' => $this->sHouseNumber,
886 'Postcode' => $this->sPostcode
890 public function dumpAsHtmlTableRow(&$aWordIDs)
892 $kf = function ($k) use (&$aWordIDs) {
893 return $aWordIDs[$k] ?? '['.$k.']';
897 echo "<td>$this->iSearchRank</td>";
898 echo '<td>'.join(', ', array_map($kf, $this->aName)).'</td>';
899 echo '<td>'.join(', ', array_map($kf, $this->aNameNonSearch)).'</td>';
900 echo '<td>'.join(', ', array_map($kf, $this->aAddress)).'</td>';
901 echo '<td>'.join(', ', array_map($kf, $this->aAddressNonSearch)).'</td>';
902 echo '<td>'.$this->sCountryCode.'</td>';
903 echo '<td>'.Operator::toString($this->iOperator).'</td>';
904 echo '<td>'.$this->sClass.'</td>';
905 echo '<td>'.$this->sType.'</td>';
906 echo '<td>'.$this->sPostcode.'</td>';
907 echo '<td>'.$this->sHouseNumber.'</td>';