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