]> git.openstreetmap.org Git - nominatim.git/blob - lib/Geocode.php
Merge remote-tracking branch 'upstream/master'
[nominatim.git] / lib / Geocode.php
1 <?php
2         class Geocode
3         {
4                 protected $oDB;
5
6                 protected $aLangPrefOrder = array();
7
8                 protected $bIncludeAddressDetails = false;
9
10                 protected $bIncludePolygonAsPoints = false;
11                 protected $bIncludePolygonAsText = false;
12                 protected $bIncludePolygonAsGeoJSON = false;
13                 protected $bIncludePolygonAsKML = false;
14                 protected $bIncludePolygonAsSVG = false;
15
16                 protected $aExcludePlaceIDs = array();
17                 protected $bDeDupe = true;
18                 protected $bReverseInPlan = true;
19
20                 protected $iLimit = 20;
21                 protected $iFinalLimit = 10;
22                 protected $iOffset = 0;
23                 protected $bFallback = false;
24
25                 protected $aCountryCodes = false;
26                 protected $aNearPoint = false;
27
28                 protected $bBoundedSearch = false;
29                 protected $aViewBox = false;
30                 protected $sViewboxSmallSQL = false;
31                 protected $sViewboxLargeSQL = false;
32                 protected $aRoutePoints = false;
33
34                 protected $iMaxRank = 20;
35                 protected $iMinAddressRank = 0;
36                 protected $iMaxAddressRank = 30;
37                 protected $aAddressRankList = array();
38                 protected $exactMatchCache = array();
39
40                 protected $sAllowedTypesSQLList = false;
41
42                 protected $sQuery = false;
43                 protected $aStructuredQuery = false;
44
45                 function Geocode(&$oDB)
46                 {
47                         $this->oDB =& $oDB;
48                 }
49
50                 function setReverseInPlan($bReverse)
51                 {
52                         $this->bReverseInPlan = $bReverse;
53                 }
54
55                 function setLanguagePreference($aLangPref)
56                 {
57                         $this->aLangPrefOrder = $aLangPref;
58                 }
59
60                 function setIncludeAddressDetails($bAddressDetails = true)
61                 {
62                         $this->bIncludeAddressDetails = (bool)$bAddressDetails;
63                 }
64
65                 function getIncludeAddressDetails()
66                 {
67                         return $this->bIncludeAddressDetails;
68                 }
69
70                 function setIncludePolygonAsPoints($b = true)
71                 {
72                         $this->bIncludePolygonAsPoints = $b;
73                 }
74
75                 function getIncludePolygonAsPoints()
76                 {
77                         return $this->bIncludePolygonAsPoints;
78                 }
79
80                 function setIncludePolygonAsText($b = true)
81                 {
82                         $this->bIncludePolygonAsText = $b;
83                 }
84
85                 function getIncludePolygonAsText()
86                 {
87                         return $this->bIncludePolygonAsText;
88                 }
89
90                 function setIncludePolygonAsGeoJSON($b = true)
91                 {
92                         $this->bIncludePolygonAsGeoJSON = $b;
93                 }
94
95                 function setIncludePolygonAsKML($b = true)
96                 {
97                         $this->bIncludePolygonAsKML = $b;
98                 }
99
100                 function setIncludePolygonAsSVG($b = true)
101                 {
102                         $this->bIncludePolygonAsSVG = $b;
103                 }
104
105                 function setDeDupe($bDeDupe = true)
106                 {
107                         $this->bDeDupe = (bool)$bDeDupe;
108                 }
109
110                 function setLimit($iLimit = 10)
111                 {
112                         if ($iLimit > 50) $iLimit = 50;
113                         if ($iLimit < 1) $iLimit = 1;
114
115                         $this->iFinalLimit = $iLimit;
116                         $this->iLimit = $this->iFinalLimit + min($this->iFinalLimit, 10);
117                 }
118
119                 function setOffset($iOffset = 0)
120                 {
121                         $this->iOffset = $iOffset;
122                 }
123
124                 function setFallback($bFallback = true)
125                 {
126                         $this->bFallback = (bool)$bFallback;
127                 }
128
129                 function setExcludedPlaceIDs($a)
130                 {
131                         // TODO: force to int
132                         $this->aExcludePlaceIDs = $a;
133                 }
134
135                 function getExcludedPlaceIDs()
136                 {
137                         return $this->aExcludePlaceIDs;
138                 }
139
140                 function setBounded($bBoundedSearch = true)
141                 {
142                         $this->bBoundedSearch = (bool)$bBoundedSearch;
143                 }
144
145                 function setViewBox($fLeft, $fBottom, $fRight, $fTop)
146                 {
147                         $this->aViewBox = array($fLeft, $fBottom, $fRight, $fTop);
148                 }
149
150                 function getViewBoxString()
151                 {
152                         if (!$this->aViewBox) return null;
153                         return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
154                 }
155
156                 function setRoute($aRoutePoints)
157                 {
158                         $this->aRoutePoints = $aRoutePoints;
159                 }
160
161                 function setFeatureType($sFeatureType)
162                 {
163                         switch($sFeatureType)
164                         {
165                         case 'country':
166                                 $this->setRankRange(4, 4);
167                                 break;
168                         case 'state':
169                                 $this->setRankRange(8, 8);
170                                 break;
171                         case 'city':
172                                 $this->setRankRange(14, 16);
173                                 break;
174                         case 'settlement':
175                                 $this->setRankRange(8, 20);
176                                 break;
177                         }
178                 }
179
180                 function setRankRange($iMin, $iMax)
181                 {
182                         $this->iMinAddressRank = (int)$iMin;
183                         $this->iMaxAddressRank = (int)$iMax;
184                 }
185
186                 function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
187                 {
188                         $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
189                 }
190
191                 function setCountryCodesList($aCountryCodes)
192                 {
193                         $this->aCountryCodes = $aCountryCodes;
194                 }
195
196                 function setQuery($sQueryString)
197                 {
198                         $this->sQuery = $sQueryString;
199                         $this->aStructuredQuery = false;
200                 }
201
202                 function getQueryString()
203                 {
204                         return $this->sQuery;
205                 }
206
207                 function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
208                 {
209                         $sValue = trim($sValue);
210                         if (!$sValue) return false;
211                         $this->aStructuredQuery[$sKey] = $sValue;
212                         if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30)
213                         {
214                                 $this->iMinAddressRank = $iNewMinAddressRank;
215                                 $this->iMaxAddressRank = $iNewMaxAddressRank;
216                         }
217                         if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
218                         return true;
219                 }
220
221                 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
222                 {
223                         $this->sQuery = false;
224
225                         // Reset
226                         $this->iMinAddressRank = 0;
227                         $this->iMaxAddressRank = 30;
228                         $this->aAddressRankList = array();
229
230                         $this->aStructuredQuery = array();
231                         $this->sAllowedTypesSQLList = '';
232
233                         $this->loadStructuredAddressElement($sAmentiy, 'amenity', 26, 30, false);
234                         $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
235                         $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
236                         $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
237                         $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
238                         $this->loadStructuredAddressElement($sPostalCode, 'postalcode' , 5, 11, array(5, 11));
239                         $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
240
241                         if (sizeof($this->aStructuredQuery) > 0) 
242                         {
243                                 $this->sQuery = join(', ', $this->aStructuredQuery);
244                                 if ($this->iMaxAddressRank < 30)
245                                 {
246                                         $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
247                                 }
248                         }
249                 }
250
251                 function fallbackStructuredQuery()
252                 {
253                         if (!$this->aStructuredQuery) return false;
254
255                         $aParams = $this->aStructuredQuery;
256
257                         if (sizeof($aParams) == 1) return false;
258
259                         $aOrderToFallback = array('postalcode', 'street', 'city', 'county', 'state');
260
261                         foreach($aOrderToFallback as $sType)
262                         {
263                                 if (isset($aParams[$sType]))
264                                 {
265                                         unset($aParams[$sType]);
266                                         $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
267                                         return true;
268                                 }
269                         }
270
271                         return false;
272                 }
273
274                 function getDetails($aPlaceIDs)
275                 {
276                         if (sizeof($aPlaceIDs) == 0)  return array();
277
278                         $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
279
280                         // Get the details for display (is this a redundant extra step?)
281                         $sPlaceIDs = join(',',$aPlaceIDs);
282
283                         $sImportanceSQL = '';
284                         if ($this->sViewboxSmallSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
285                         if ($this->sViewboxLargeSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
286
287                         $sSQL = "select osm_type,osm_id,class,type,admin_level,rank_search,rank_address,min(place_id) as place_id, min(parent_place_id) as parent_place_id, calculated_country_code as country_code,";
288                         $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
289                         $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
290                         $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
291                         $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
292                         $sSQL .= $sImportanceSQL."coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
293                         $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(CASE WHEN placex.rank_search < 28 THEN placex.place_id ELSE placex.parent_place_id END) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
294                         $sSQL .= "(extratags->'place') as extra_place ";
295                         $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
296                         $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
297                         if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
298                         if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
299                         $sSQL .= ") ";
300                         if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->sAllowedTypesSQLList ";
301                         $sSQL .= "and linked_place_id is null ";
302                         $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
303                         if (!$this->bDeDupe) $sSQL .= ",place_id";
304                         $sSQL .= ",langaddress ";
305                         $sSQL .= ",placename ";
306                         $sSQL .= ",ref ";
307                         $sSQL .= ",extratags->'place' ";
308
309                         if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank)
310                         {
311                                 $sSQL .= " union ";
312                                 $sSQL .= "select 'T' as osm_type,place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id, min(parent_place_id) as parent_place_id,'us' as country_code,";
313                                 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
314                                 $sSQL .= "null as placename,";
315                                 $sSQL .= "null as ref,";
316                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
317                                 $sSQL .= $sImportanceSQL."-1.15 as importance, ";
318                                 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(location_property_tiger.parent_place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
319                                 $sSQL .= "null as extra_place ";
320                                 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
321                                 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
322                                 $sSQL .= "group by place_id";
323                                 if (!$this->bDeDupe) $sSQL .= ",place_id ";
324                                 /*
325                                 $sSQL .= " union ";
326                                 $sSQL .= "select 'L' as osm_type,place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id, min(parent_place_id) as parent_place_id,'us' as country_code,";
327                                 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
328                                 $sSQL .= "null as placename,";
329                                 $sSQL .= "null as ref,";
330                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
331                                 $sSQL .= $sImportanceSQL."-1.10 as importance, ";
332                                 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(location_property_aux.parent_place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
333                                 $sSQL .= "null as extra_place ";
334                                 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
335                                 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
336                                 $sSQL .= "group by place_id";
337                                 if (!$this->bDeDupe) $sSQL .= ",place_id";
338                                 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
339                                 */
340                         }
341
342                         $sSQL .= " order by importance desc";
343                         if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
344                         $aSearchResults = $this->oDB->getAll($sSQL);
345
346                         if (PEAR::IsError($aSearchResults))
347                         {
348                                 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
349                         }
350
351                         return $aSearchResults;
352                 }
353
354                 /* Perform the actual query lookup.
355
356                         Returns an ordered list of results, each with the following fields:
357                           osm_type: type of corresponding OSM object
358                                                         N - node
359                                                         W - way
360                                                         R - relation
361                                                         P - postcode (internally computed)
362                           osm_id: id of corresponding OSM object
363                           class: general object class (corresponds to tag key of primary OSM tag)
364                           type: subclass of object (corresponds to tag value of primary OSM tag)
365                           admin_level: see http://wiki.openstreetmap.org/wiki/Admin_level
366                           rank_search: rank in search hierarchy
367                                                         (see also http://wiki.openstreetmap.org/wiki/Nominatim/Development_overview#Country_to_street_level)
368                           rank_address: rank in address hierarchy (determines orer in address)
369                           place_id: internal key (may differ between different instances)
370                           country_code: ISO country code
371                           langaddress: localized full address
372                           placename: localized name of object
373                           ref: content of ref tag (if available)
374                           lon: longitude
375                           lat: latitude
376                           importance: importance of place based on Wikipedia link count
377                           addressimportance: cumulated importance of address elements
378                           extra_place: type of place (for admin boundaries, if there is a place tag)
379                           aBoundingBox: bounding Box
380                           label: short description of the object class/type (English only) 
381                           name: full name (currently the same as langaddress)
382                           foundorder: secondary ordering for places with same importance
383                 */
384                 function lookup()
385                 {
386                         if (!$this->sQuery && !$this->aStructuredQuery) return false;
387
388                         $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
389
390                         $sCountryCodesSQL = false;
391                         if ($this->aCountryCodes && sizeof($this->aCountryCodes))
392                         {
393                                 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
394                         }
395
396                         // Hack to make it handle "new york, ny" (and variants) correctly
397                         //$sQuery = str_ireplace(array('New York, ny','new york, new york', 'New York ny','new york new york'), 'new york city, ny', $this->sQuery);
398                         $sQuery = $this->sQuery;
399
400                         // Conflicts between US state abreviations and various words for 'the' in different languages
401                         if (isset($this->aLangPrefOrder['name:en']))
402                         {
403                                 $sQuery = preg_replace('/,\s*il\s*(,|$)/',', illinois\1', $sQuery);
404                                 $sQuery = preg_replace('/,\s*al\s*(,|$)/',', alabama\1', $sQuery);
405                                 $sQuery = preg_replace('/,\s*la\s*(,|$)/',', louisiana\1', $sQuery);
406                         }
407
408                         // View Box SQL
409                         $sViewboxCentreSQL;
410                         $bBoundingBoxSearch = false;
411                         if ($this->aViewBox)
412                         {
413                                 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
414                                 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
415                                 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
416                                 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
417                                 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
418                                 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
419
420                                 $this->sViewboxSmallSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$this->aViewBox[0].",".(float)$this->aViewBox[1]."),ST_Point(".(float)$this->aViewBox[2].",".(float)$this->aViewBox[3].")),4326)";
421                                 $this->sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
422                                 $bBoundingBoxSearch = $this->bBoundedSearch;
423                         }
424
425                         // Route SQL
426                         if ($this->aRoutePoints)
427                         {
428                                 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
429                                 $bFirst = false;
430                                 foreach($this->aRouteaPoints as $aPoint)
431                                 {
432                                         if (!$bFirst) $sViewboxCentreSQL .= ",";
433                                         $sViewboxCentreSQL .= $aPoint[1].' '.$aPoint[0];
434                                 }
435                                 $sViewboxCentreSQL .= ")'::geometry,4326)";
436
437                                 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
438                                 $this->sViewboxSmallSQL = $this->oDB->getOne($sSQL);
439                                 if (PEAR::isError($this->sViewboxSmallSQL))
440                                 {
441                                         failInternalError("Could not get small viewbox.", $sSQL, $this->sViewboxSmallSQL);
442                                 }
443                                 $this->sViewboxSmallSQL = "'".$this->sViewboxSmallSQL."'::geometry";
444
445                                 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
446                                 $this->sViewboxLargeSQL = $this->oDB->getOne($sSQL);
447                                 if (PEAR::isError($this->sViewboxLargeSQL))
448                                 {
449                                         failInternalError("Could not get large viewbox.", $sSQL, $this->sViewboxLargeSQL);
450                                 }
451                                 $this->sViewboxLargeSQL = "'".$this->sViewboxLargeSQL."'::geometry";
452                                 $bBoundingBoxSearch = $this->bBoundedSearch;
453                         }
454
455                         // Do we have anything that looks like a lat/lon pair?
456                         if (preg_match('/\\b([NS])[ ]+([0-9]+[0-9.]*)[ ]+([0-9.]+)?[, ]+([EW])[ ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?\\b/', $sQuery, $aData))
457                         {
458                                 $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60);
459                                 $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60);
460                                 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
461                                 {
462                                         $this->setNearPoint(array($fQueryLat, $fQueryLon));
463                                         $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
464                                 }
465                         }
466                         elseif (preg_match('/\\b([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([NS])[, ]+([0-9]+)[ ]+([0-9]+[0-9.]*)?[ ]+([EW])\\b/', $sQuery, $aData))
467                         {
468                                 $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60);
469                                 $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60);
470                                 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
471                                 {
472                                         $this->setNearPoint(array($fQueryLat, $fQueryLon));
473                                         $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
474                                 }
475                         }
476                         elseif (preg_match('/(\\[|^|\\b)(-?[0-9]+[0-9]*\\.[0-9]+)[, ]+(-?[0-9]+[0-9]*\\.[0-9]+)(\\]|$|\\b)/', $sQuery, $aData))
477                         {
478                                 $fQueryLat = $aData[2];
479                                 $fQueryLon = $aData[3];
480                                 if ($fQueryLat <= 90.1 && $fQueryLat >= -90.1 && $fQueryLon <= 180.1 && $fQueryLon >= -180.1)
481                                 {
482                                         $this->setNearPoint(array($fQueryLat, $fQueryLon));
483                                         $sQuery = trim(str_replace($aData[0], ' ', $sQuery));
484                                 }
485                         }
486
487                         $aSearchResults = array();
488                         if ($sQuery || $this->aStructuredQuery)
489                         {
490                                 // Start with a blank search
491                                 $aSearches = array(
492                                         array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
493                                               'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
494                                               'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
495                                 );
496
497                                 // Do we have a radius search?
498                                 $sNearPointSQL = false;
499                                 if ($this->aNearPoint)
500                                 {
501                                         $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
502                                         $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
503                                         $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
504                                         $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
505                                 }
506
507                                 // Any 'special' terms in the search?
508                                 $bSpecialTerms = false;
509                                 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
510                                 $aSpecialTerms = array();
511                                 foreach($aSpecialTermsRaw as $aSpecialTerm)
512                                 {
513                                         $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
514                                         $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
515                                 }
516
517                                 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
518                                 $aSpecialTerms = array();
519                                 if (isset($aStructuredQuery['amenity']) && $aStructuredQuery['amenity'])
520                                 {
521                                         $aSpecialTermsRaw[] = array('['.$aStructuredQuery['amenity'].']', $aStructuredQuery['amenity']);
522                                         unset($aStructuredQuery['amenity']);
523                                 }
524                                 foreach($aSpecialTermsRaw as $aSpecialTerm)
525                                 {
526                                         $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
527                                         $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
528                                         $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
529                                         $sSQL .= ' from word where word_token in (\' '.$sToken.'\')) as x where (class is not null and class not in (\'place\')) or country_code is not null';
530                                         if (CONST_Debug) var_Dump($sSQL);
531                                         $aSearchWords = $this->oDB->getAll($sSQL);
532                                         $aNewSearches = array();
533                                         foreach($aSearches as $aSearch)
534                                         {
535                                                 foreach($aSearchWords as $aSearchTerm)
536                                                 {
537                                                         $aNewSearch = $aSearch;
538                                                         if ($aSearchTerm['country_code'])
539                                                         {
540                                                                 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
541                                                                 $aNewSearches[] = $aNewSearch;
542                                                                 $bSpecialTerms = true;
543                                                         }
544                                                         if ($aSearchTerm['class'])
545                                                         {
546                                                                 $aNewSearch['sClass'] = $aSearchTerm['class'];
547                                                                 $aNewSearch['sType'] = $aSearchTerm['type'];
548                                                                 $aNewSearches[] = $aNewSearch;
549                                                                 $bSpecialTerms = true;
550                                                         }
551                                                 }
552                                         }
553                                         $aSearches = $aNewSearches;
554                                 }
555
556                                 // Split query into phrases
557                                 // Commas are used to reduce the search space by indicating where phrases split
558                                 if ($this->aStructuredQuery)
559                                 {
560                                         $aPhrases = $this->aStructuredQuery;
561                                         $bStructuredPhrases = true;
562                                 }
563                                 else
564                                 {
565                                         $aPhrases = explode(',',$sQuery);
566                                         $bStructuredPhrases = false;
567                                 }
568
569                                 // Convert each phrase to standard form
570                                 // Create a list of standard words
571                                 // Get all 'sets' of words
572                                 // Generate a complete list of all
573                                 $aTokens = array();
574                                 foreach($aPhrases as $iPhrase => $sPhrase)
575                                 {
576                                         $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
577                                         if (PEAR::isError($aPhrase))
578                                         {
579                                                 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
580                                                 if (CONST_Debug) var_dump($aPhrase);
581                                                 exit;
582                                         }
583                                         if (trim($aPhrase['string']))
584                                         {
585                                                 $aPhrases[$iPhrase] = $aPhrase;
586                                                 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
587                                                 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
588                                                 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
589                                         }
590                                         else
591                                         {
592                                                 unset($aPhrases[$iPhrase]);
593                                         }
594                                 }
595
596                                 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
597                                 $aPhraseTypes = array_keys($aPhrases);
598                                 $aPhrases = array_values($aPhrases);
599
600                                 if (sizeof($aTokens))
601                                 {
602                                         // Check which tokens we have, get the ID numbers
603                                         $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
604                                         $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
605
606                                         if (CONST_Debug) var_Dump($sSQL);
607
608                                         $aValidTokens = array();
609                                         if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
610                                         else $aDatabaseWords = array();
611                                         if (PEAR::IsError($aDatabaseWords))
612                                         {
613                                                 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
614                                         }
615                                         $aPossibleMainWordIDs = array();
616                                         $aWordFrequencyScores = array();
617                                         foreach($aDatabaseWords as $aToken)
618                                         {
619                                                 // Very special case - require 2 letter country param to match the country code found
620                                                 if ($bStructuredPhrases && $aToken['country_code'] && !empty($aStructuredQuery['country'])
621                                                                 && strlen($aStructuredQuery['country']) == 2 && strtolower($aStructuredQuery['country']) != $aToken['country_code'])
622                                                 {
623                                                         continue;
624                                                 }
625
626                                                 if (isset($aValidTokens[$aToken['word_token']]))
627                                                 {
628                                                         $aValidTokens[$aToken['word_token']][] = $aToken;
629                                                 }
630                                                 else
631                                                 {
632                                                         $aValidTokens[$aToken['word_token']] = array($aToken);
633                                                 }
634                                                 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
635                                                 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
636                                         }
637                                         if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
638
639                                         // Try and calculate GB postcodes we might be missing
640                                         foreach($aTokens as $sToken)
641                                         {
642                                                 // Source of gb postcodes is now definitive - always use
643                                                 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
644                                                 {
645                                                         if (substr($aData[1],-2,1) != ' ')
646                                                         {
647                                                                 $aData[0] = substr($aData[0],0,strlen($aData[1]-1)).' '.substr($aData[0],strlen($aData[1]-1));
648                                                                 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
649                                                         }
650                                                         $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
651                                                         if ($aGBPostcodeLocation)
652                                                         {
653                                                                 $aValidTokens[$sToken] = $aGBPostcodeLocation;
654                                                         }
655                                                 }
656                                                 // US ZIP+4 codes - if there is no token,
657                                                 //      merge in the 5-digit ZIP code
658                                                 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
659                                                 {
660                                                         if (isset($aValidTokens[$aData[1]]))
661                                                         {
662                                                                 foreach($aValidTokens[$aData[1]] as $aToken)
663                                                                 {
664                                                                         if (!$aToken['class'])
665                                                                         {
666                                                                                 if (isset($aValidTokens[$sToken]))
667                                                                                 {
668                                                                                         $aValidTokens[$sToken][] = $aToken;
669                                                                                 }
670                                                                                 else
671                                                                                 {
672                                                                                         $aValidTokens[$sToken] = array($aToken);
673                                                                                 }
674                                                                         }
675                                                                 }
676                                                         }
677                                                 }
678                                         }
679
680                                         foreach($aTokens as $sToken)
681                                         {
682                                                 // Unknown single word token with a number - assume it is a house number
683                                                 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
684                                                 {
685                                                         $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
686                                                 }
687                                         }
688
689                                         // Any words that have failed completely?
690                                         // TODO: suggestions
691
692                                         // Start the search process
693                                         $aResultPlaceIDs = array();
694
695                                         /*
696                                            Calculate all searches using aValidTokens i.e.
697                                            'Wodsworth Road, Sheffield' =>
698
699                                            Phrase Wordset
700                                            0      0       (wodsworth road)
701                                            0      1       (wodsworth)(road)
702                                            1      0       (sheffield)
703
704                                            Score how good the search is so they can be ordered
705                                          */
706                                         foreach($aPhrases as $iPhrase => $sPhrase)
707                                         {
708                                                 $aNewPhraseSearches = array();
709                                                 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
710                                                 else $sPhraseType = '';
711
712                                                 foreach($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset)
713                                                 {
714                                                         // Too many permutations - too expensive
715                                                         if ($iWordSet > 120) break;
716
717                                                         $aWordsetSearches = $aSearches;
718
719                                                         // Add all words from this wordset
720                                                         foreach($aWordset as $iToken => $sToken)
721                                                         {
722                                                                 //echo "<br><b>$sToken</b>";
723                                                                 $aNewWordsetSearches = array();
724
725                                                                 foreach($aWordsetSearches as $aCurrentSearch)
726                                                                 {
727                                                                         //echo "<i>";
728                                                                         //var_dump($aCurrentSearch);
729                                                                         //echo "</i>";
730
731                                                                         // If the token is valid
732                                                                         if (isset($aValidTokens[' '.$sToken]))
733                                                                         {
734                                                                                 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
735                                                                                 {
736                                                                                         $aSearch = $aCurrentSearch;
737                                                                                         $aSearch['iSearchRank']++;
738                                                                                         if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
739                                                                                         {
740                                                                                                 if ($aSearch['sCountryCode'] === false)
741                                                                                                 {
742                                                                                                         $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
743                                                                                                         // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
744                                                                                                         // If reverse order is enabled, it may appear at the beginning as well.
745                                                                                                         if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)) &&
746                                                                                                                         (!$this->bReverseInPlan || $iToken > 0 || $iPhrase > 0))
747                                                                                                         {
748                                                                                                                 $aSearch['iSearchRank'] += 5;
749                                                                                                         }
750                                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
751                                                                                                 }
752                                                                                         }
753                                                                                         elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
754                                                                                         {
755                                                                                                 if ($aSearch['fLat'] === '')
756                                                                                                 {
757                                                                                                         $aSearch['fLat'] = $aSearchTerm['lat'];
758                                                                                                         $aSearch['fLon'] = $aSearchTerm['lon'];
759                                                                                                         $aSearch['fRadius'] = $aSearchTerm['radius'];
760                                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
761                                                                                                 }
762                                                                                         }
763                                                                                         elseif ($sPhraseType == 'postalcode')
764                                                                                         {
765                                                                                                 // We need to try the case where the postal code is the primary element (i.e. no way to tell if it is (postalcode, city) OR (city, postalcode) so try both
766                                                                                                 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
767                                                                                                 {
768                                                                                                         // If we already have a name try putting the postcode first
769                                                                                                         if (sizeof($aSearch['aName']))
770                                                                                                         {
771                                                                                                                 $aNewSearch = $aSearch;
772                                                                                                                 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
773                                                                                                                 $aNewSearch['aName'] = array();
774                                                                                                                 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
775                                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
776                                                                                                         }
777
778                                                                                                         if (sizeof($aSearch['aName']))
779                                                                                                         {
780                                                                                                                 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
781                                                                                                                 {
782                                                                                                                         $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
783                                                                                                                 }
784                                                                                                                 else
785                                                                                                                 {
786                                                                                                                         $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
787                                                                                                                         $aSearch['iSearchRank'] += 1000; // skip;
788                                                                                                                 }
789                                                                                                         }
790                                                                                                         else
791                                                                                                         {
792                                                                                                                 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
793                                                                                                                 //$aSearch['iNamePhrase'] = $iPhrase;
794                                                                                                         }
795                                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
796                                                                                                 }
797
798                                                                                         }
799                                                                                         elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
800                                                                                         {
801                                                                                                 if ($aSearch['sHouseNumber'] === '')
802                                                                                                 {
803                                                                                                         $aSearch['sHouseNumber'] = $sToken;
804                                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
805                                                                                                         /*
806                                                                                                         // Fall back to not searching for this item (better than nothing)
807                                                                                                         $aSearch = $aCurrentSearch;
808                                                                                                         $aSearch['iSearchRank'] += 1;
809                                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
810                                                                                                          */
811                                                                                                 }
812                                                                                         }
813                                                                                         elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
814                                                                                         {
815                                                                                                 if ($aSearch['sClass'] === '')
816                                                                                                 {
817                                                                                                         $aSearch['sOperator'] = $aSearchTerm['operator'];
818                                                                                                         $aSearch['sClass'] = $aSearchTerm['class'];
819                                                                                                         $aSearch['sType'] = $aSearchTerm['type'];
820                                                                                                         if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
821                                                                                                         else $aSearch['sOperator'] = 'near'; // near = in for the moment
822                                                                                                         if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
823
824                                                                                                         // Do we have a shortcut id?
825                                                                                                         if ($aSearch['sOperator'] == 'name')
826                                                                                                         {
827                                                                                                                 $sSQL = "select get_tagpair('".$aSearch['sClass']."', '".$aSearch['sType']."')";
828                                                                                                                 if ($iAmenityID = $this->oDB->getOne($sSQL))
829                                                                                                                 {
830                                                                                                                         $aValidTokens[$aSearch['sClass'].':'.$aSearch['sType']] = array('word_id' => $iAmenityID);
831                                                                                                                         $aSearch['aName'][$iAmenityID] = $iAmenityID;
832                                                                                                                         $aSearch['sClass'] = '';
833                                                                                                                         $aSearch['sType'] = '';
834                                                                                                                 }
835                                                                                                         }
836                                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
837                                                                                                 }
838                                                                                         }
839                                                                                         elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
840                                                                                         {
841                                                                                                 if (sizeof($aSearch['aName']))
842                                                                                                 {
843                                                                                                         if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strlen($sToken) < 4 || strpos($sToken, ' ') !== false))
844                                                                                                         {
845                                                                                                                 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
846                                                                                                         }
847                                                                                                         else
848                                                                                                         {
849                                                                                                                 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
850                                                                                                                 $aSearch['iSearchRank'] += 1000; // skip;
851                                                                                                         }
852                                                                                                 }
853                                                                                                 else
854                                                                                                 {
855                                                                                                         $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
856                                                                                                         //$aSearch['iNamePhrase'] = $iPhrase;
857                                                                                                 }
858                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
859                                                                                         }
860                                                                                 }
861                                                                         }
862                                                                         if (isset($aValidTokens[$sToken]))
863                                                                         {
864                                                                                 // Allow searching for a word - but at extra cost
865                                                                                 foreach($aValidTokens[$sToken] as $aSearchTerm)
866                                                                                 {
867                                                                                         if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
868                                                                                         {
869                                                                                                 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strlen($sToken) >= 4)
870                                                                                                 {
871                                                                                                         $aSearch = $aCurrentSearch;
872                                                                                                         $aSearch['iSearchRank'] += 1;
873                                                                                                         if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
874                                                                                                         {
875                                                                                                                 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
876                                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
877                                                                                                         }
878                                                                                                         elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
879                                                                                                         {
880                                                                                                                 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
881                                                                                                                 {
882                                                                                                                         if (empty($aSearchTermToken['country_code'])
883                                                                                                                                         && empty($aSearchTermToken['lat'])
884                                                                                                                                         && empty($aSearchTermToken['class']))
885                                                                                                                         {
886                                                                                                                                 $aSearch = $aCurrentSearch;
887                                                                                                                                 $aSearch['iSearchRank'] += 1;
888                                                                                                                                 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
889                                                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
890                                                                                                                         }
891                                                                                                                 }
892                                                                                                         }
893                                                                                                         else
894                                                                                                         {
895                                                                                                                 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
896                                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
897                                                                                                         }
898                                                                                                 }
899
900                                                                                                 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
901                                                                                                 {
902                                                                                                         $aSearch = $aCurrentSearch;
903                                                                                                         $aSearch['iSearchRank'] += 2;
904                                                                                                         if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
905                                                                                                         if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
906                                                                                                                 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
907                                                                                                         else
908                                                                                                                 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
909                                                                                                         $aSearch['iNamePhrase'] = $iPhrase;
910                                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
911                                                                                                 }
912                                                                                         }
913                                                                                 }
914                                                                         }
915                                                                         else
916                                                                         {
917                                                                                 // Allow skipping a word - but at EXTREAM cost
918                                                                                 //$aSearch = $aCurrentSearch;
919                                                                                 //$aSearch['iSearchRank']+=100;
920                                                                                 //$aNewWordsetSearches[] = $aSearch;
921                                                                         }
922                                                                 }
923                                                                 // Sort and cut
924                                                                 usort($aNewWordsetSearches, 'bySearchRank');
925                                                                 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
926                                                         }
927                                                         //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
928
929                                                         $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
930                                                         usort($aNewPhraseSearches, 'bySearchRank');
931
932                                                         $aSearchHash = array();
933                                                         foreach($aNewPhraseSearches as $iSearch => $aSearch)
934                                                         {
935                                                                 $sHash = serialize($aSearch);
936                                                                 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
937                                                                 else $aSearchHash[$sHash] = 1;
938                                                         }
939
940                                                         $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
941                                                 }
942
943                                                 // Re-group the searches by their score, junk anything over 20 as just not worth trying
944                                                 $aGroupedSearches = array();
945                                                 foreach($aNewPhraseSearches as $aSearch)
946                                                 {
947                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank)
948                                                         {
949                                                                 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
950                                                                 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
951                                                         }
952                                                 }
953                                                 ksort($aGroupedSearches);
954
955                                                 $iSearchCount = 0;
956                                                 $aSearches = array();
957                                                 foreach($aGroupedSearches as $iScore => $aNewSearches)
958                                                 {
959                                                         $iSearchCount += sizeof($aNewSearches);
960                                                         $aSearches = array_merge($aSearches, $aNewSearches);
961                                                         if ($iSearchCount > 50) break;
962                                                 }
963
964                                                 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
965
966                                         }
967
968                                 }
969                                 else
970                                 {
971                                         // Re-group the searches by their score, junk anything over 20 as just not worth trying
972                                         $aGroupedSearches = array();
973                                         foreach($aSearches as $aSearch)
974                                         {
975                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank)
976                                                 {
977                                                         if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
978                                                         $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
979                                                 }
980                                         }
981                                         ksort($aGroupedSearches);
982                                 }
983
984                                 if (CONST_Debug) var_Dump($aGroupedSearches);
985
986                                 if ($this->bReverseInPlan)
987                                 {
988                                         $aCopyGroupedSearches = $aGroupedSearches;
989                                         foreach($aCopyGroupedSearches as $iGroup => $aSearches)
990                                         {
991                                                 foreach($aSearches as $iSearch => $aSearch)
992                                                 {
993                                                         if (sizeof($aSearch['aAddress']))
994                                                         {
995                                                                 $iReverseItem = array_pop($aSearch['aAddress']);
996                                                                 if (isset($aPossibleMainWordIDs[$iReverseItem]))
997                                                                 {
998                                                                         $aSearch['aAddress'] = array_merge($aSearch['aAddress'], $aSearch['aName']);
999                                                                         $aSearch['aName'] = array($iReverseItem);
1000                                                                         $aGroupedSearches[$iGroup][] = $aSearch;
1001                                                                 }
1002                                                                 //$aReverseSearch['aName'][$iReverseItem] = $iReverseItem;
1003                                                                 //$aGroupedSearches[$iGroup][] = $aReverseSearch;
1004                                                         }
1005                                                 }
1006                                         }
1007                                 }
1008
1009                                 if (CONST_Search_TryDroppedAddressTerms && sizeof($aStructuredQuery) > 0)
1010                                 {
1011                                         $aCopyGroupedSearches = $aGroupedSearches;
1012                                         foreach($aCopyGroupedSearches as $iGroup => $aSearches)
1013                                         {
1014                                                 foreach($aSearches as $iSearch => $aSearch)
1015                                                 {
1016                                                         $aReductionsList = array($aSearch['aAddress']);
1017                                                         $iSearchRank = $aSearch['iSearchRank'];
1018                                                         while(sizeof($aReductionsList) > 0)
1019                                                         {
1020                                                                 $iSearchRank += 5;
1021                                                                 if ($iSearchRank > iMaxRank) break 3;
1022                                                                 $aNewReductionsList = array();
1023                                                                 foreach($aReductionsList as $aReductionsWordList)
1024                                                                 {
1025                                                                         for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
1026                                                                         {
1027                                                                                 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
1028                                                                                 $aReverseSearch = $aSearch;
1029                                                                                 $aSearch['aAddress'] = $aReductionsWordListResult;
1030                                                                                 $aSearch['iSearchRank'] = $iSearchRank;
1031                                                                                 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
1032                                                                                 if (sizeof($aReductionsWordListResult) > 0)
1033                                                                                 {
1034                                                                                         $aNewReductionsList[] = $aReductionsWordListResult;
1035                                                                                 }
1036                                                                         }
1037                                                                 }
1038                                                                 $aReductionsList = $aNewReductionsList;
1039                                                         }
1040                                                 }
1041                                         }
1042                                         ksort($aGroupedSearches);
1043                                 }
1044
1045                                 // Filter out duplicate searches
1046                                 $aSearchHash = array();
1047                                 foreach($aGroupedSearches as $iGroup => $aSearches)
1048                                 {
1049                                         foreach($aSearches as $iSearch => $aSearch)
1050                                         {
1051                                                 $sHash = serialize($aSearch);
1052                                                 if (isset($aSearchHash[$sHash]))
1053                                                 {
1054                                                         unset($aGroupedSearches[$iGroup][$iSearch]);
1055                                                         if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
1056                                                 }
1057                                                 else
1058                                                 {
1059                                                         $aSearchHash[$sHash] = 1;
1060                                                 }
1061                                         }
1062                                 }
1063
1064                                 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1065
1066                                 $iGroupLoop = 0;
1067                                 $iQueryLoop = 0;
1068                                 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
1069                                 {
1070                                         $iGroupLoop++;
1071                                         foreach($aSearches as $aSearch)
1072                                         {
1073                                                 $iQueryLoop++;
1074
1075                                                 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
1076                                                 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1077
1078                                                 // No location term?
1079                                                 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
1080                                                 {
1081                                                         if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
1082                                                         {
1083                                                                 // Just looking for a country by code - look it up
1084                                                                 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
1085                                                                 {
1086                                                                         $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
1087                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1088                                                                         $sSQL .= " order by st_area(geometry) desc limit 1";
1089                                                                         if (CONST_Debug) var_dump($sSQL);
1090                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1091                                                                 }
1092                                                         }
1093                                                         else
1094                                                         {
1095                                                                 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1096                                                                 if (!$aSearch['sClass']) continue;
1097                                                                 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1098                                                                 if ($this->oDB->getOne($sSQL))
1099                                                                 {
1100                                                                         $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1101                                                                         if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1102                                                                         $sSQL .= " where st_contains($this->sViewboxSmallSQL, ct.centroid)";
1103                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1104                                                                         if (sizeof($this->aExcludePlaceIDs))
1105                                                                         {
1106                                                                                 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1107                                                                         }
1108                                                                         if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1109                                                                         $sSQL .= " limit $this->iLimit";
1110                                                                         if (CONST_Debug) var_dump($sSQL);
1111                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1112
1113                                                                         // If excluded place IDs are given, it is fair to assume that
1114                                                                         // there have been results in the small box, so no further
1115                                                                         // expansion in that case.
1116                                                                         if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs))
1117                                                                         {
1118                                                                                 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1119                                                                                 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1120                                                                                 $sSQL .= " where st_contains($this->sViewboxLargeSQL, ct.centroid)";
1121                                                                                 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1122                                                                                 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1123                                                                                 $sSQL .= " limit $this->iLimit";
1124                                                                                 if (CONST_Debug) var_dump($sSQL);
1125                                                                                 $aPlaceIDs = $this->oDB->getCol($sSQL);
1126                                                                         }
1127                                                                 }
1128                                                                 else
1129                                                                 {
1130                                                                         $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1131                                                                         $sSQL .= " and st_contains($this->sViewboxSmallSQL, geometry) and linked_place_id is null";
1132                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1133                                                                         if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1134                                                                         $sSQL .= " limit $this->iLimit";
1135                                                                         if (CONST_Debug) var_dump($sSQL);
1136                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1137                                                                 }
1138                                                         }
1139                                                 }
1140                                                 else
1141                                                 {
1142                                                         $aPlaceIDs = array();
1143
1144                                                         // First we need a position, either aName or fLat or both
1145                                                         $aTerms = array();
1146                                                         $aOrder = array();
1147
1148                                                         // TODO: filter out the pointless search terms (2 letter name tokens and less)
1149                                                         // they might be right - but they are just too darned expensive to run
1150                                                         if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1151                                                         //if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1152                                                         if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1153                                                         {
1154                                                                 // For infrequent name terms disable index usage for address
1155                                                                 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1156                                                                                 sizeof($aSearch['aName']) == 1 &&
1157                                                                                 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1158                                                                 {
1159                                                                         //$aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1160                                                                         $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddress'],",")."]";
1161                                                                 }
1162                                                                 else
1163                                                                 {
1164                                                                         $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1165                                                                         //if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1166                                                                 }
1167                                                         }
1168                                                         if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1169                                                         if ($aSearch['sHouseNumber']) $aTerms[] = "address_rank between 16 and 27";
1170                                                         if ($aSearch['fLon'] && $aSearch['fLat'])
1171                                                         {
1172                                                                 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1173                                                                 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1174                                                         }
1175                                                         if (sizeof($this->aExcludePlaceIDs))
1176                                                         {
1177                                                                 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1178                                                         }
1179                                                         if ($sCountryCodesSQL)
1180                                                         {
1181                                                                 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1182                                                         }
1183
1184                                                         if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
1185                                                         if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1186
1187                                                         $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1188                                                         if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1189                                                         if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1190                                                         $aOrder[] = "$sImportanceSQL DESC";
1191                                                         if (sizeof($aSearch['aFullNameAddress']))
1192                                                         {
1193                                                                 $sExactMatchSQL = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) as exactmatch';
1194                                                                 $aOrder[] = 'exactmatch DESC';
1195                                                         } else {
1196                                                                 $sExactMatchSQL = '0::int as exactmatch';
1197                                                         }
1198
1199                                                         if (sizeof($aTerms))
1200                                                         {
1201                                                                 $sSQL = "select place_id, ";
1202                                                                 $sSQL .= $sExactMatchSQL;
1203                                                                 $sSQL .= " from search_name";
1204                                                                 $sSQL .= " where ".join(' and ',$aTerms);
1205                                                                 $sSQL .= " order by ".join(', ',$aOrder);
1206                                                                 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1207                                                                         $sSQL .= " limit 50";
1208                                                                 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1209                                                                         $sSQL .= " limit 1";
1210                                                                 else
1211                                                                         $sSQL .= " limit ".$this->iLimit;
1212
1213                                                                 if (CONST_Debug) { var_dump($sSQL); }
1214                                                                 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1215                                                                 if (PEAR::IsError($aViewBoxPlaceIDs))
1216                                                                 {
1217                                                                         failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1218                                                                 }
1219                                                                 //var_dump($aViewBoxPlaceIDs);
1220                                                                 // Did we have an viewbox matches?
1221                                                                 $aPlaceIDs = array();
1222                                                                 $bViewBoxMatch = false;
1223                                                                 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1224                                                                 {
1225                                                                         //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1226                                                                         //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1227                                                                         //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1228                                                                         //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1229                                                                         $aPlaceIDs[] = $aViewBoxRow['place_id'];
1230                                                                         $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch'];
1231                                                                 }
1232                                                         }
1233                                                         //var_Dump($aPlaceIDs);
1234                                                         //exit;
1235
1236                                                         if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1237                                                         {
1238                                                                 $aRoadPlaceIDs = $aPlaceIDs;
1239                                                                 $sPlaceIDs = join(',',$aPlaceIDs);
1240
1241                                                                 // Now they are indexed look for a house attached to a street we found
1242                                                                 $sHouseNumberRegex = '\\\\m'.str_replace(' ','[-,/ ]',$aSearch['sHouseNumber']).'\\\\M';
1243                                                                 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and housenumber ~* E'".$sHouseNumberRegex."'";
1244                                                                 if (sizeof($this->aExcludePlaceIDs))
1245                                                                 {
1246                                                                         $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1247                                                                 }
1248                                                                 $sSQL .= " limit $this->iLimit";
1249                                                                 if (CONST_Debug) var_dump($sSQL);
1250                                                                 $aPlaceIDs = $this->oDB->getCol($sSQL);
1251
1252                                                                 // If not try the aux fallback table
1253                                                                 /*
1254                                                                 if (!sizeof($aPlaceIDs))
1255                                                                 {
1256                                                                         $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1257                                                                         if (sizeof($this->aExcludePlaceIDs))
1258                                                                         {
1259                                                                                 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1260                                                                         }
1261                                                                         //$sSQL .= " limit $this->iLimit";
1262                                                                         if (CONST_Debug) var_dump($sSQL);
1263                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1264                                                                 }
1265                                                                 */
1266
1267                                                                 if (!sizeof($aPlaceIDs))
1268                                                                 {
1269                                                                         $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1270                                                                         if (sizeof($this->aExcludePlaceIDs))
1271                                                                         {
1272                                                                                 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1273                                                                         }
1274                                                                         //$sSQL .= " limit $this->iLimit";
1275                                                                         if (CONST_Debug) var_dump($sSQL);
1276                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1277                                                                 }
1278
1279                                                                 // Fallback to the road
1280                                                                 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1281                                                                 {
1282                                                                         $aPlaceIDs = $aRoadPlaceIDs;
1283                                                                 }
1284
1285                                                         }
1286
1287                                                         if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1288                                                         {
1289                                                                 $sPlaceIDs = join(',',$aPlaceIDs);
1290                                                                 $aClassPlaceIDs = array();
1291
1292                                                                 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1293                                                                 {
1294                                                                         // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1295                                                                         $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1296                                                                         $sSQL .= " and linked_place_id is null";
1297                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1298                                                                         $sSQL .= " order by rank_search asc limit $this->iLimit";
1299                                                                         if (CONST_Debug) var_dump($sSQL);
1300                                                                         $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1301                                                                 }
1302
1303                                                                 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1304                                                                 {
1305                                                                         $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1306                                                                         $bCacheTable = $this->oDB->getOne($sSQL);
1307
1308                                                                         $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1309
1310                                                                         if (CONST_Debug) var_dump($sSQL);
1311                                                                         $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1312
1313                                                                         // For state / country level searches the normal radius search doesn't work very well
1314                                                                         $sPlaceGeom = false;
1315                                                                         if ($this->iMaxRank < 9 && $bCacheTable)
1316                                                                         {
1317                                                                                 // Try and get a polygon to search in instead
1318                                                                                 $sSQL = "select geometry from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank + 5 and st_geometrytype(geometry) in ('ST_Polygon','ST_MultiPolygon') order by rank_search asc limit 1";
1319                                                                                 if (CONST_Debug) var_dump($sSQL);
1320                                                                                 $sPlaceGeom = $this->oDB->getOne($sSQL);
1321                                                                         }
1322
1323                                                                         if ($sPlaceGeom)
1324                                                                         {
1325                                                                                 $sPlaceIDs = false;
1326                                                                         }
1327                                                                         else
1328                                                                         {
1329                                                                                 $this->iMaxRank += 5;
1330                                                                                 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1331                                                                                 if (CONST_Debug) var_dump($sSQL);
1332                                                                                 $aPlaceIDs = $this->oDB->getCol($sSQL);
1333                                                                                 $sPlaceIDs = join(',',$aPlaceIDs);
1334                                                                         }
1335
1336                                                                         if ($sPlaceIDs || $sPlaceGeom)
1337                                                                         {
1338
1339                                                                                 $fRange = 0.01;
1340                                                                                 if ($bCacheTable)
1341                                                                                 {
1342                                                                                         // More efficient - can make the range bigger
1343                                                                                         $fRange = 0.05;
1344
1345                                                                                         $sOrderBySQL = '';
1346                                                                                         if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1347                                                                                         else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1348                                                                                         else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1349
1350                                                                                         $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1351                                                                                         if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1352                                                                                         if ($sPlaceIDs)
1353                                                                                         {
1354                                                                                                 $sSQL .= ",placex as f where ";
1355                                                                                                 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1356                                                                                         }
1357                                                                                         if ($sPlaceGeom)
1358                                                                                         {
1359                                                                                                 $sSQL .= " where ";
1360                                                                                                 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1361                                                                                         }
1362                                                                                         if (sizeof($this->aExcludePlaceIDs))
1363                                                                                         {
1364                                                                                                 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1365                                                                                         }
1366                                                                                         if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1367                                                                                         if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1368                                                                                         if ($iOffset) $sSQL .= " offset $iOffset";
1369                                                                                         $sSQL .= " limit $this->iLimit";
1370                                                                                         if (CONST_Debug) var_dump($sSQL);
1371                                                                                         $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1372                                                                                 }
1373                                                                                 else
1374                                                                                 {
1375                                                                                         if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1376
1377                                                                                         $sOrderBySQL = '';
1378                                                                                         if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1379                                                                                         else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1380
1381                                                                                         $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1382                                                                                         $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1383                                                                                         $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1384                                                                                         if (sizeof($this->aExcludePlaceIDs))
1385                                                                                         {
1386                                                                                                 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1387                                                                                         }
1388                                                                                         if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1389                                                                                         if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1390                                                                                         if ($iOffset) $sSQL .= " offset $iOffset";
1391                                                                                         $sSQL .= " limit $this->iLimit";
1392                                                                                         if (CONST_Debug) var_dump($sSQL);
1393                                                                                         $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1394                                                                                 }
1395                                                                         }
1396                                                                 }
1397
1398                                                                 $aPlaceIDs = $aClassPlaceIDs;
1399
1400                                                         }
1401
1402                                                 }
1403
1404                                                 if (PEAR::IsError($aPlaceIDs))
1405                                                 {
1406                                                         failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1407                                                 }
1408
1409                                                 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1410
1411                                                 foreach($aPlaceIDs as $iPlaceID)
1412                                                 {
1413                                                         $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1414                                                 }
1415                                                 if ($iQueryLoop > 20) break;
1416                                         }
1417
1418                                         if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1419                                         {
1420                                                 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1421                                                 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1422                                                 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1423                                                 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1424                                                 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1425                                                 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1426                                                 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1427                                                 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1428                                                 $sSQL .= ")";
1429                                                 if (CONST_Debug) var_dump($sSQL);
1430                                                 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1431                                         }
1432
1433                                         //exit;
1434                                         if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1435                                         if ($iGroupLoop > 4) break;
1436                                         if ($iQueryLoop > 30) break;
1437                                 }
1438
1439                                 // Did we find anything?
1440                                 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1441                                 {
1442                                         $aSearchResults = $this->getDetails($aResultPlaceIDs);
1443                                 }
1444
1445                         }
1446                         else
1447                         {
1448                                 // Just interpret as a reverse geocode
1449                                 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1450                                 if ($iPlaceID)
1451                                         $aSearchResults = $this->getDetails(array($iPlaceID));
1452                                 else
1453                                         $aSearchResults = array();
1454                         }
1455
1456                         // No results? Done
1457                         if (!sizeof($aSearchResults))
1458                         {
1459                                 if ($this->bFallback)
1460                                 {
1461                                         if ($this->fallbackStructuredQuery())
1462                                         {
1463                                                 return $this->lookup();
1464                                         }
1465                                 }
1466
1467                                 return array();
1468                         }
1469
1470                         $aClassType = getClassTypesWithImportance();
1471                         $aRecheckWords = preg_split('/\b/u',$sQuery);
1472                         foreach($aRecheckWords as $i => $sWord)
1473                         {
1474                                 if (!$sWord) unset($aRecheckWords[$i]);
1475                         }
1476
1477                         foreach($aSearchResults as $iResNum => $aResult)
1478                         {
1479                                 if (CONST_Search_AreaPolygons)
1480                                 {
1481                                         // Get the bounding box and outline polygon
1482                                         $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1483                                         $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1484                                         $sSQL .= "ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),4)) as minlat,ST_Y(ST_PointN(ST_ExteriorRing(Box2D(geometry)),2)) as maxlat,";
1485                                         $sSQL .= "ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),1)) as minlon,ST_X(ST_PointN(ST_ExteriorRing(Box2D(geometry)),3)) as maxlon";
1486                                         if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1487                                         if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1488                                         if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1489                                         if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1490                                         $sSQL .= " from placex where place_id = ".$aResult['place_id'].' and st_geometrytype(Box2D(geometry)) = \'ST_Polygon\'';
1491                                         $aPointPolygon = $this->oDB->getRow($sSQL);
1492                                         if (PEAR::IsError($aPointPolygon))
1493                                         {
1494                                                 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1495                                         }
1496
1497                                         if ($aPointPolygon['place_id'])
1498                                         {
1499                                                 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1500                                                 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1501                                                 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1502                                                 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1503
1504                                                 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1505                                                 {
1506                                                         $aResult['lat'] = $aPointPolygon['centrelat'];
1507                                                         $aResult['lon'] = $aPointPolygon['centrelon'];
1508                                                 }
1509
1510                                                 if ($this->bIncludePolygonAsPoints)
1511                                                 {
1512                                                         // Translate geometary string to point array
1513                                                         if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1514                                                         {
1515                                                                 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1516                                                         }
1517                             /*
1518                                                         elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1519                                                         {
1520                                                                 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1521                                                         }
1522                             */
1523                                                         elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1524                                                         {
1525                                                                 $fRadius = 0.01;
1526                                                                 $iSteps = ($fRadius * 40000)^2;
1527                                                                 $fStepSize = (2*pi())/$iSteps;
1528                                                                 $aPolyPoints = array();
1529                                                                 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1530                                                                 {
1531                                                                         $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1532                                                                 }
1533                                                                 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1534                                                                 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1535                                                                 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1536                                                                 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1537                                                         }
1538                                                 }
1539
1540                                                 // Output data suitable for display (points and a bounding box)
1541                                                 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1542                                                 {
1543                                                         $aResult['aPolyPoints'] = array();
1544                                                         foreach($aPolyPoints as $aPoint)
1545                                                         {
1546                                                                 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1547                                                         }
1548                                                 }
1549                                                 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1550                                         }
1551                                 }
1552
1553                                 if ($aResult['extra_place'] == 'city')
1554                                 {
1555                                         $aResult['class'] = 'place';
1556                                         $aResult['type'] = 'city';
1557                                         $aResult['rank_search'] = 16;
1558                                 }
1559
1560                                 if (!isset($aResult['aBoundingBox']))
1561                                 {
1562                                         // Default
1563                                         $fDiameter = 0.0001;
1564
1565                                         if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1566                                                         && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1567                                         {
1568                                                 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1569                                         }
1570                                         elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1571                                                         && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1572                                         {
1573                                                 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1574                                         }
1575                                         $fRadius = $fDiameter / 2;
1576
1577                                         $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1578                                         $fStepSize = (2*pi())/$iSteps;
1579                                         $aPolyPoints = array();
1580                                         for($f = 0; $f < 2*pi(); $f += $fStepSize)
1581                                         {
1582                                                 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1583                                         }
1584                                         $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1585                                         $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1586                                         $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1587                                         $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1588
1589                                         // Output data suitable for display (points and a bounding box)
1590                                         if ($this->bIncludePolygonAsPoints)
1591                                         {
1592                                                 $aResult['aPolyPoints'] = array();
1593                                                 foreach($aPolyPoints as $aPoint)
1594                                                 {
1595                                                         $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1596                                                 }
1597                                         }
1598                                         $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1599                                 }
1600
1601                                 // Is there an icon set for this type of result?
1602                                 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1603                                                 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1604                                 {
1605                                         $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1606                                 }
1607
1608                                 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1609                                                 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1610                                 {
1611                                         $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1612                                 }
1613                                 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1614                                                 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1615                                 {
1616                                         $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1617                                 }
1618
1619                                 if ($this->bIncludeAddressDetails)
1620                                 {
1621                                         $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1622                                         if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1623                                         {
1624                                                 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1625                                         }
1626                                 }
1627
1628                                 // Adjust importance for the number of exact string matches in the result
1629                                 $aResult['importance'] = max(0.001,$aResult['importance']);
1630                                 $iCountWords = 0;
1631                                 $sAddress = $aResult['langaddress'];
1632                                 foreach($aRecheckWords as $i => $sWord)
1633                                 {
1634                                         if (stripos($sAddress, $sWord)!==false) $iCountWords++;
1635                                 }
1636
1637                                 $aResult['importance'] = $aResult['importance'] + ($iCountWords*0.1); // 0.1 is a completely arbitrary number but something in the range 0.1 to 0.5 would seem right
1638
1639                                 $aResult['name'] = $aResult['langaddress'];
1640                                 // secondary ordering (for results with same importance (the smaller the better):
1641                                 //   - approximate importance of address parts
1642                                 $aResult['foundorder'] = -$aResult['addressimportance']/10;
1643                                 //   - number of exact matches from the query
1644                                 if (isset($this->exactMatchCache[$aResult['place_id']]))
1645                                         $aResult['foundorder'] -= $this->exactMatchCache[$aResult['place_id']];
1646                                 else if (isset($this->exactMatchCache[$aResult['parent_place_id']]))
1647                                         $aResult['foundorder'] -= $this->exactMatchCache[$aResult['parent_place_id']];
1648                                 //  - importance of the class/type
1649                                 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1650                                         && $aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1651                                 {
1652                                         $aResult['foundorder'] = $aResult['foundorder'] + 0.000001 * $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1653                                 }
1654                                 else
1655                                 {
1656                                         $aResult['foundorder'] = $aResult['foundorder'] + 0.001;
1657                                 }
1658                                 $aSearchResults[$iResNum] = $aResult;
1659                         }
1660                         uasort($aSearchResults, 'byImportance');
1661
1662                         $aOSMIDDone = array();
1663                         $aClassTypeNameDone = array();
1664                         $aToFilter = $aSearchResults;
1665                         $aSearchResults = array();
1666
1667                         $bFirst = true;
1668                         foreach($aToFilter as $iResNum => $aResult)
1669                         {
1670                                 if ($aResult['type'] == 'adminitrative') $aResult['type'] = 'administrative';
1671                                 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1672                                 if ($bFirst)
1673                                 {
1674                                         $fLat = $aResult['lat'];
1675                                         $fLon = $aResult['lon'];
1676                                         if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1677                                         $bFirst = false;
1678                                 }
1679                                 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1680                                                         && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1681                                 {
1682                                         $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1683                                         $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1684                                         $aSearchResults[] = $aResult;
1685                                 }
1686
1687                                 // Absolute limit on number of results
1688                                 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1689                         }
1690
1691                         return $aSearchResults;
1692
1693                 } // end lookup()
1694
1695
1696         } // end class
1697
1698
1699 /*
1700                 if (isset($_GET['route']) && $_GET['route'] && isset($_GET['routewidth']) && $_GET['routewidth'])
1701                 {
1702                         $aPoints = explode(',',$_GET['route']);
1703                         if (sizeof($aPoints) % 2 != 0)
1704                         {
1705                                 userError("Uneven number of points");
1706                                 exit;
1707                         }
1708                         $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
1709                         $fPrevCoord = false;
1710                 }
1711 */