6 * Operators describing special searches.
8 abstract final class Operator
10 /// No operator selected.
12 /// Search for POI of the given type.
14 /// Search for POIs near the given place.
16 /// Search for POIS in the given place.
18 /// Search for POIS named as given.
20 /// Search for postcodes.
25 * Description of a single interpretation of a search query.
27 class SearchDescription
29 /// Ranking how well the description fits the query.
30 private $iSearchRank = 0;
31 /// Country code of country the result must belong to.
32 private $sCountryCode = '';
33 /// List of word ids making up the name of the object.
34 private $aName = array();
35 /// List of word ids making up the address of the object.
36 private $aAddress = array();
37 /// Subset of word ids of full words making up the address.
38 private $aFullNameAddress = array();
39 /// List of word ids that appear in the name but should be ignored.
40 private $aNameNonSearch = array();
41 /// List of word ids that appear in the address but should be ignored.
42 private $aAddressNonSearch = array();
43 /// Kind of search for special searches, see Nominatim::Operator.
44 private $iOperator = Operator::NONE;
45 /// Class of special feature to search for.
47 /// Type of special feature to search for.
49 /// Housenumber of the object.
50 private $sHouseNumber = '';
51 /// Postcode for the object.
52 private $sPostcode = '';
53 /// Geographic search area.
54 private $oNearPoint = false;
56 // Temporary values used while creating the search description.
58 /// Index of phrase currently processed
59 private $iNamePhrase = -1;
61 public function getRank()
63 return $this->iSearchRank;
66 public function getPostCode()
68 return $this->sPostcode;
72 * Set the geographic search radius.
74 public function setNear(&$oNearPoint)
76 $this->oNearPoint = $oNearPoint;
79 public function setPoiSearch($iOperator, $sClass, $sType)
81 $this->iOperator = $iOperator;
82 $this->sClass = $sClass;
83 $this->sType = $sType;
87 * Check if name or address for the search are specified.
89 public function isNamedSearch()
91 return sizeof($this->aName) > 0 || sizeof($this->aAddress) > 0;
95 * Check if only a country is requested.
97 public function isCountrySearch()
99 return $this->sCountryCode && sizeof($this->aName) == 0
100 && !$this->iOperator && !$this->oNear;
104 * Check if a search near a geographic location is requested.
106 public function isNearSearch()
108 return (bool) $this->oNear;
111 public function isPoiSearch()
113 return (bool) $this->sClass;
116 public function looksLikeFullAddress()
118 return sizeof($this->aName)
119 && (sizeof($this->aAddress || $this->sCountryCode))
120 && preg_match('/[0-9]+/', $this->sHouseNumber);
123 public function isOperator($iType)
125 return $this->iOperator == $iType;
128 public function hasHouseNumber()
130 return (bool) $this->sHouseNumber;
133 private function poiTable()
135 return 'place_classtype_'.$this->sClass.'_'.$this->sType;
138 public function countryCodeSQL($sVar, $sCountryList)
140 if ($this->sCountryCode) {
141 return $sVar.' = \''.$this->sCountryCode."'";
144 return $sVar.' in ('.$this->sCountryCode.')';
150 public function hasOperator()
152 return $this->iOperator != Operator::NONE;
156 * Extract special terms from the query, amend the search
157 * and return the shortended query.
159 * Only the first special term found will be used but all will
160 * be removed from the query.
162 public function extractKeyValuePairs($sQuery)
164 // Search for terms of kind [<key>=<value>].
166 '/\\[([\\w_]*)=([\\w_]*)\\]/',
172 foreach ($aSpecialTermsRaw as $aTerm) {
173 $sQuery = str_replace($aTerm[0], ' ', $sQuery);
174 if (!$this->hasOperator()) {
175 $this->setPoiSearch(Operator::TYPE, $aTerm[1], $aTerm[2]);
182 public function queryCountry(&$oDB, $sViewboxSQL)
184 $sSQL = 'SELECT place_id FROM placex ';
185 $sSQL .= "WHERE country_code='".$this->sCountryCode."'";
186 $sSQL .= ' AND rank_search = 4';
188 $sSQL .= " AND ST_Intersects($sViewboxSQL, geometry)";
190 $sSQL .= " ORDER BY st_area(geometry) DESC LIMIT 1";
192 if (CONST_Debug) var_dump($sSQL);
194 return chksql($oDB->getCol($sSQL));
197 public function queryNearbyPoi(&$oDB, $sCountryList, $sViewboxSQL, $sViewboxCentreSQL, $sExcludeSQL, $iLimit)
199 if (!$this->sClass) {
203 $sPoiTable = $this->poiTable();
205 $sSQL = 'SELECT count(*) FROM pg_tables WHERE tablename = \''.$sPoiTable."'";
206 if (chksql($oDB->getOne($sSQL))) {
207 $sSQL = 'SELECT place_id FROM '.$sPoiTable.' ct';
209 $sSQL .= ' JOIN placex USING (place_id)';
211 if ($this->oNearPoint) {
212 $sSQL .= ' WHERE '.$this->oNearPoint->withinSQL('ct.centroid');
214 $sSQL .= " WHERE ST_Contains($sViewboxSQL, ct.centroid)";
217 $sSQL .= " AND country_code in ($sCountryList)";
220 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
222 if ($sViewboxCentreSQL) {
223 $sSQL .= " ORDER BY ST_Distance($sViewboxCentreSQL, ct.centroid) ASC";
224 } elseif ($this->oNearPoint) {
225 $sSQL .= ' ORDER BY '.$this->oNearPoint->distanceSQL('ct.centroid').' ASC';
227 $sSQL .= " limit $iLimit";
228 if (CONST_Debug) var_dump($sSQL);
229 return chksql($this->oDB->getCol($sSQL));
232 if ($this->oNearPoint) {
233 $sSQL = 'SELECT place_id FROM placex WHERE ';
234 $sSQL .= 'class=\''.$this->sClass."' and type='".$this->sType."'";
235 $sSQL .= ' AND '.$this->oNearPoint->withinSQL('geometry');
236 $sSQL .= ' AND linked_place_id is null';
238 $sSQL .= " AND country_code in ($sCountryList)";
240 $sSQL .= ' ORDER BY '.$this->oNearPoint->distanceSQL('centroid')." ASC";
241 $sSQL .= " LIMIT $iLimit";
242 if (CONST_Debug) var_dump($sSQL);
243 return chksql($this->oDB->getCol($sSQL));
249 public function queryPostcode(&$oDB, $sCountryList, $iLimit)
251 $sSQL = 'SELECT p.place_id FROM location_postcode p ';
253 if (sizeof($this->aAddress)) {
254 $sSQL .= ', search_name s ';
255 $sSQL .= 'WHERE s.place_id = p.parent_place_id ';
256 $sSQL .= 'AND array_cat(s.nameaddress_vector, s.name_vector)';
257 $sSQL .= ' @> '.getArraySQL($this->aAddress).' AND ';
262 $sSQL .= "p.postcode = '".pg_escape_string(reset($this->$aName))."'";
263 $sCountryTerm = $this->countryCodeSQL('p.country_code', $sCountryList);
265 $sSQL .= ' AND '.$sCountyTerm;
267 $sSQL .= " LIMIT $iLimit";
269 if (CONST_Debug) var_dump($sSQL);
271 return chksql($this->oDB->getCol($sSQL));
274 public function queryNamedPlace(&$oDB, $aWordFrequencyScores, $sCountryList, $iMinAddressRank, $iMaxAddressRank, $sExcludeSQL, $sViewboxSmall, $sViewboxLarge, $iLimit)
279 if ($this->sHouseNumber && sizeof($this->aAddress)) {
280 $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
282 $aOrder[0] .= 'EXISTS(';
283 $aOrder[0] .= ' SELECT place_id';
284 $aOrder[0] .= ' FROM placex';
285 $aOrder[0] .= ' WHERE parent_place_id = search_name.place_id';
286 $aOrder[0] .= " AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
287 $aOrder[0] .= ' LIMIT 1';
289 // also housenumbers from interpolation lines table are needed
290 if (preg_match('/[0-9]+/', $this->sHouseNumber)) {
291 $iHouseNumber = intval($this->sHouseNumber);
292 $aOrder[0] .= 'OR EXISTS(';
293 $aOrder[0] .= ' SELECT place_id ';
294 $aOrder[0] .= ' FROM location_property_osmline ';
295 $aOrder[0] .= ' WHERE parent_place_id = search_name.place_id';
296 $aOrder[0] .= ' AND startnumber is not NULL';
297 $aOrder[0] .= ' AND '.$iHouseNumber.'>=startnumber ';
298 $aOrder[0] .= ' AND '.$iHouseNumber.'<=endnumber ';
299 $aOrder[0] .= ' LIMIT 1';
302 $aOrder[0] .= ') DESC';
305 if (sizeof($this->aName)) {
306 $aTerms[] = 'name_vector @> '.getArraySQL($this->aName);
308 if (sizeof($this->aAddress)) {
309 // For infrequent name terms disable index usage for address
310 if (CONST_Search_NameOnlySearchFrequencyThreshold
311 && sizeof($this->aName) == 1
312 && $aWordFrequencyScores[$this->aName[reset($this->aName)]]
313 < CONST_Search_NameOnlySearchFrequencyThreshold
315 $aTerms[] = 'array_cat(nameaddress_vector,ARRAY[]::integer[]) @> '.getArraySQL($this->aAddress);
317 $aTerms[] = 'nameaddress_vector @> '.getArraySQL($this->aAddress);
321 $sCountryTerm = $this->countryCodeSQL('p.country_code', $sCountryList);
323 $aTerms[] = $sCountryTerm;
326 if ($this->sHouseNumber) {
327 $aTerms[] = "address_rank between 16 and 27";
328 } elseif (!$this->sClass || $this->iOperator == Operator::NAME) {
329 if ($iMinAddressRank > 0) {
330 $aTerms[] = "address_rank >= ".$iMinAddressRank;
332 if ($iMaxAddressRank < 30) {
333 $aTerms[] = "address_rank <= ".$iMaxAddressRank;
337 if ($this->oNearPoint) {
338 $aTerms[] = $this->oNearPoint->withinSQL('centroid');
339 $aOrder[] = $this->oNearPoint->distanceSQL('centroid');
340 } elseif ($this->sPostcode) {
341 if (!sizeof($this->aAddress)) {
342 $aTerms[] = "EXISTS(SELECT place_id FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."' AND ST_DWithin(search_name.centroid, p.geometry, 0.1))";
344 $aOrder[] = "(SELECT min(ST_Distance(search_name.centroid, p.geometry)) FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."')";
349 $aTerms = 'place_id not in ('.$sExcludeSQL.')';
352 if ($sViewboxSmall) {
353 $aTerms[] = 'centroid && '.$sViewboxSmall;
356 if ($this->oNearPoint) {
357 $aOrder[] = $this->oNearPoint->distanceSQL('centroid');
360 if ($this->sHouseNumber) {
361 $sImportanceSQL = '- abs(26 - address_rank) + 3';
363 $sImportanceSQL = '(CASE WHEN importance = 0 OR importance IS NULL THEN 0.75-(search_rank::float/40) ELSE importance END)';
365 if ($sViewboxSmall) {
366 $sImportanceSQL .= " * CASE WHEN ST_Contains($sViewboxSmall, centroid) THEN 1 ELSE 0.5 END";
368 if ($sViewboxLarge) {
369 $sImportanceSQL .= " * CASE WHEN ST_Contains($sViewboxLarge, centroid) THEN 1 ELSE 0.5 END";
371 $aOrder[] = "$sImportanceSQL DESC";
373 if (sizeof($this->aFullNameAddress)) {
374 $sExactMatchSQL = ' ( ';
375 $sExactMatchSQL .= ' SELECT count(*) FROM ( ';
376 $sExactMatchSQL .= ' SELECT unnest('.getArraySQL($this->aFullNameAddress).')';
377 $sExactMatchSQL .= ' INTERSECT ';
378 $sExactMatchSQL .= ' SELECT unnest(nameaddress_vector)';
379 $sExactMatchSQL .= ' ) s';
380 $sExactMatchSQL .= ') as exactmatch';
381 $aOrder[] = 'exactmatch DESC';
383 $sExactMatchSQL = '0::int as exactmatch';
386 if ($this->sHouseNumber || $this->sClass) {
390 if (sizeof($aTerms)) {
391 $sSQL = 'SELECT place_id,'.$sExactMatchSQL;
392 $sSQL .= ' FROM search_name';
393 $sSQL .= ' WHERE '.join(' and ', $aTerms);
394 $sSQL .= ' ORDER BY '.join(', ', $aOrder);
395 $sSQL .= ' LIMIT '.$iLimit;
397 if (CONST_Debug) var_dump($sSQL);
400 $this->oDB->getAll($sSQL),
401 "Could not get places for search terms."
409 public function queryHouseNumber(&$oDB, $aRoadPlaceIDs, $sExcludeSQL, $iLimit)
411 $sPlaceIDs = join(',', $aRoadPlaceIDs);
413 $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
414 $sSQL = 'SELECT place_id FROM placex ';
415 $sSQL .= 'WHERE parent_place_id in ('.$sPlaceIDs.')';
416 $sSQL .= " AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
418 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
420 $sSQL .= " LIMIT $iLimit";
422 if (CONST_Debug) var_dump($sSQL);
424 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
426 if (sizeof($aPlaceIDs)) {
427 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => -1);
430 $bIsIntHouseNumber= (bool) preg_match('/[0-9]+/', $this->sHouseNumber);
431 $iHousenumber = intval($this->sHouseNumber);
432 if ($bIsIntHouseNumber) {
433 // if nothing found, search in the interpolation line table
434 $sSQL = 'SELECT distinct place_id FROM location_property_osmline';
435 $sSQL .= ' WHERE startnumber is not NULL';
436 $sSQL .= ' AND parent_place_id in ('.$sPlaceIDs.') AND (';
437 if ($iHousenumber % 2 == 0) {
438 // If housenumber is even, look for housenumber in streets
439 // with interpolationtype even or all.
440 $sSQL .= "interpolationtype='even'";
442 // Else look for housenumber with interpolationtype odd or all.
443 $sSQL .= "interpolationtype='odd'";
445 $sSQL .= " or interpolationtype='all') and ";
446 $sSQL .= $iHousenumber.">=startnumber and ";
447 $sSQL .= $iHousenumber."<=endnumber";
450 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
452 $sSQL .= " limit $iLimit";
454 if (CONST_Debug) var_dump($sSQL);
456 $aPlaceIDs = chksql($this->oDB->getCol($sSQL, 0));
458 if (sizeof($aPlaceIDs)) {
459 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => $iHousenumber);
463 // If nothing found try the aux fallback table
464 if (CONST_Use_Aux_Location_data) {
465 $sSQL = 'SELECT place_id FROM location_property_aux';
466 $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.')';
467 $sSQL .= " AND housenumber = '".$this->sHouseNumber."'";
469 $sSQL .= " AND place_id not in ($sExcludeSQL)";
471 $sSQL .= " limit $iLimit";
473 if (CONST_Debug) var_dump($sSQL);
475 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
477 if (sizeof($aPlaceIDs)) {
478 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => -1);
482 // If nothing found then search in Tiger data (location_property_tiger)
483 if (CONST_Use_US_Tiger_Data && $bIsIntHouseNumber) {
484 $sSQL = 'SELECT distinct place_id FROM location_property_tiger';
485 $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.') and (';
486 if ($iHousenumber % 2 == 0) {
487 $sSQL .= "interpolationtype='even'";
489 $sSQL .= "interpolationtype='odd'";
491 $sSQL .= " or interpolationtype='all') and ";
492 $sSQL .= $iHousenumber.">=startnumber and ";
493 $sSQL .= $iHousenumber."<=endnumber";
496 $sSQL .= ' AND place_id not in ('.$sExcludeSQL.')';
498 $sSQL .= " limit $iLimit";
500 if (CONST_Debug) var_dump($sSQL);
502 $aPlaceIDs = chksql($this->oDB->getCol($sSQL, 0));
504 if (sizeof($aPlaceIDs)) {
505 return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => $iHousenumber);
513 public function queryPoiByOperator(&$oDB, $aParentIDs, $sExcludeSQL, $iLimit)
515 $sPlaceIDs = join(',', $aParentIDs);
516 $aClassPlaceIDs = array();
518 if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NAME) {
519 // If they were searching for a named class (i.e. 'Kings Head pub')
520 // then we might have an extra match
521 $sSQL = 'SELECT place_id FROM placex ';
522 $sSQL .= " WHERE place_id in ($sPlaceIDs)";
523 $sSQL .= " AND class='".$this->sClass."' ";
524 $sSQL .= " AND type='".$this->sType."'";
525 $sSQL .= " AND linked_place_id is null";
526 $sSQL .= " ORDER BY rank_search ASC ";
527 $sSQL .= " LIMIT $iLimit";
529 if (CONST_Debug) var_dump($sSQL);
531 $aClassPlaceIDs = chksql($this->oDB->getCol($sSQL));
534 // NEAR and IN are handled the same
535 if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NEAR) {
536 $sClassTable = $this->poiTable();
537 $sSQL = "SELECT count(*) FROM pg_tables WHERE tablename = '$sClassTable'";
538 $bCacheTable = (bool) chksql($this->oDB->getOne($sSQL));
540 $sSQL = "SELECT min(rank_search) FROM placex WHERE place_id in ($sPlaceIDs)";
541 if (CONST_Debug) var_dump($sSQL);
542 $iMaxRank = (int)chksql($this->oDB->getOne($sSQL));
544 // For state / country level searches the normal radius search doesn't work very well
546 if ($iMaxRank < 9 && $bCacheTable) {
547 // Try and get a polygon to search in instead
548 $sSQL = 'SELECT geometry FROM placex';
549 $sSQL .= " WHERE place_id in ($sPlaceIDs)";
550 $sSQL .= " AND rank_search < $iMaxRank + 5";
551 $sSQL .= " AND ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon')";
552 $sSQL .= " ORDER BY rank_search ASC ";
554 if (CONST_Debug) var_dump($sSQL);
555 $sPlaceGeom = chksql($this->oDB->getOne($sSQL));
562 $sSQL = 'SELECT place_id FROM placex';
563 $sSQL .= " WHERE place_id in ($sPlaceIDs) and rank_search < $iMaxRank";
564 if (CONST_Debug) var_dump($sSQL);
565 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
566 $sPlaceIDs = join(',', $aPlaceIDs);
569 if ($sPlaceIDs || $sPlaceGeom) {
572 // More efficient - can make the range bigger
576 if ($this->oNearPoint) {
577 $sOrderBySQL = $this->oNearPoint->distanceSQL('l.centroid');
578 } elseif ($sPlaceIDs) {
579 $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
580 } elseif ($sPlaceGeom) {
581 $sOrderBySQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
584 $sSQL = 'SELECT distinct i.place_id';
586 $sSQL .= ', i.order_term';
588 $sSQL .= ' from (SELECT l.place_id';
590 $sSQL .= ','.$sOrderBySQL.' as order_term';
592 $sSQL .= ' from '.$sClassTable.' as l';
595 $sSQL .= ",placex as f WHERE ";
596 $sSQL .= "f.place_id in ($sPlaceIDs) ";
597 $sSQL .= " AND ST_DWithin(l.centroid, f.centroid, $fRange)";
598 } elseif ($sPlaceGeom) {
599 $sSQL .= " WHERE ST_Contains('$sPlaceGeom', l.centroid)";
603 $sSQL .= ' AND l.place_id not in ('.$sExcludeSQL.')';
605 $sSQL .= 'limit 300) i ';
607 $sSQL .= 'order by order_term asc';
609 $sSQL .= " limit $iLimit";
611 if (CONST_Debug) var_dump($sSQL);
613 $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($this->oDB->getCol($sSQL)));
615 if ($this->oNearPoint) {
616 $fRange = $this->oNearPoint->radius();
620 if ($this->oNearPoint) {
621 $sOrderBySQL = $this->oNearPoint->distanceSQL('l.geometry');
623 $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
626 $sSQL = 'SELECT distinct l.place_id';
628 $sSQL .= ','.$sOrderBySQL.' as orderterm';
630 $sSQL .= ' FROM placex as l, placex as f';
631 $sSQL .= " WHERE f.place_id in ($sPlaceIDs)";
632 $sSQL .= " AND ST_DWithin(l.geometry, f.centroid, $fRange)";
633 $sSQL .= " AND l.class='".$this->sClass."'";
634 $sSQL .= " AND l.type='".$this->sType."'";
636 $sSQL .= " AND l.place_id not in (".$sExcludeSQL.")";
639 $sSQL .= "ORDER BY orderterm ASC";
641 $sSQL .= " limit $iLimit";
643 if (CONST_Debug) var_dump($sSQL);
645 $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($this->oDB->getCol($sSQL)));
650 return $aClassPlaceIDs;