]> git.openstreetmap.org Git - nominatim.git/blob - lib/setup/SetupClass.php
increase minimum versions for PostgreSQL, Postgis and PHP
[nominatim.git] / lib / setup / SetupClass.php
1 <?php
2
3 namespace Nominatim\Setup;
4
5 require_once(CONST_BasePath.'/lib/setup/AddressLevelParser.php');
6
7 class SetupFunctions
8 {
9     protected $iCacheMemory;
10     protected $iInstances;
11     protected $sModulePath;
12     protected $aDSNInfo;
13     protected $bVerbose;
14     protected $sIgnoreErrors;
15     protected $bEnableDiffUpdates;
16     protected $bEnableDebugStatements;
17     protected $bNoPartitions;
18     protected $oDB = null;
19
20     public function __construct(array $aCMDResult)
21     {
22         // by default, use all but one processor, but never more than 15.
23         $this->iInstances = isset($aCMDResult['threads'])
24             ? $aCMDResult['threads']
25             : (min(16, getProcessorCount()) - 1);
26
27         if ($this->iInstances < 1) {
28             $this->iInstances = 1;
29             warn('resetting threads to '.$this->iInstances);
30         }
31
32         // Assume we can steal all the cache memory in the box (unless told otherwise)
33         if (isset($aCMDResult['osm2pgsql-cache'])) {
34             $this->iCacheMemory = $aCMDResult['osm2pgsql-cache'];
35         } else {
36             $this->iCacheMemory = getCacheMemoryMB();
37         }
38
39         $this->sModulePath = CONST_Database_Module_Path;
40         info('module path: ' . $this->sModulePath);
41
42         // parse database string
43         $this->aDSNInfo = \Nominatim\DB::parseDSN(CONST_Database_DSN);
44         if (!isset($this->aDSNInfo['port'])) {
45             $this->aDSNInfo['port'] = 5432;
46         }
47
48         // setting member variables based on command line options stored in $aCMDResult
49         $this->bVerbose = $aCMDResult['verbose'];
50
51         //setting default values which are not set by the update.php array
52         if (isset($aCMDResult['ignore-errors'])) {
53             $this->sIgnoreErrors = $aCMDResult['ignore-errors'];
54         } else {
55             $this->sIgnoreErrors = false;
56         }
57         if (isset($aCMDResult['enable-debug-statements'])) {
58             $this->bEnableDebugStatements = $aCMDResult['enable-debug-statements'];
59         } else {
60             $this->bEnableDebugStatements = false;
61         }
62         if (isset($aCMDResult['no-partitions'])) {
63             $this->bNoPartitions = $aCMDResult['no-partitions'];
64         } else {
65             $this->bNoPartitions = false;
66         }
67         if (isset($aCMDResult['enable-diff-updates'])) {
68             $this->bEnableDiffUpdates = $aCMDResult['enable-diff-updates'];
69         } else {
70             $this->bEnableDiffUpdates = false;
71         }
72     }
73
74     public function createDB()
75     {
76         info('Create DB');
77         $oDB = new \Nominatim\DB;
78
79         if ($oDB->databaseExists()) {
80             fail('database already exists ('.CONST_Database_DSN.')');
81         }
82
83         $sCreateDBCmd = 'createdb -E UTF-8 -p '.$this->aDSNInfo['port'].' '.$this->aDSNInfo['database'];
84         if (isset($this->aDSNInfo['username'])) {
85             $sCreateDBCmd .= ' -U '.$this->aDSNInfo['username'];
86         }
87
88         if (isset($this->aDSNInfo['hostspec'])) {
89             $sCreateDBCmd .= ' -h '.$this->aDSNInfo['hostspec'];
90         }
91
92         $result = $this->runWithPgEnv($sCreateDBCmd);
93         if ($result != 0) fail('Error executing external command: '.$sCreateDBCmd);
94     }
95
96     public function connect()
97     {
98         $this->oDB = new \Nominatim\DB();
99         $this->oDB->connect();
100     }
101
102     public function setupDB()
103     {
104         info('Setup DB');
105
106         $fPostgresVersion = $this->oDB->getPostgresVersion();
107         echo 'Postgres version found: '.$fPostgresVersion."\n";
108
109         if ($fPostgresVersion < 9.03) {
110             fail('Minimum supported version of Postgresql is 9.3.');
111         }
112
113         $this->pgsqlRunScript('CREATE EXTENSION IF NOT EXISTS hstore');
114         $this->pgsqlRunScript('CREATE EXTENSION IF NOT EXISTS postgis');
115
116         $fPostgisVersion = $this->oDB->getPostgisVersion();
117         echo 'Postgis version found: '.$fPostgisVersion."\n";
118
119         if ($fPostgisVersion < 2.2) {
120             echo "Minimum required Postgis version 2.2\n";
121             exit(1);
122         }
123
124         $i = $this->oDB->getOne("select count(*) from pg_user where usename = '".CONST_Database_Web_User."'");
125         if ($i == 0) {
126             echo "\nERROR: Web user '".CONST_Database_Web_User."' does not exist. Create it with:\n";
127             echo "\n          createuser ".CONST_Database_Web_User."\n\n";
128             exit(1);
129         }
130
131         // Try accessing the C module, so we know early if something is wrong
132         checkModulePresence(); // raises exception on failure
133
134         if (!file_exists(CONST_ExtraDataPath.'/country_osm_grid.sql.gz')) {
135             echo 'Error: you need to download the country_osm_grid first:';
136             echo "\n    wget -O ".CONST_ExtraDataPath."/country_osm_grid.sql.gz https://www.nominatim.org/data/country_grid.sql.gz\n";
137             exit(1);
138         }
139         $this->pgsqlRunScriptFile(CONST_BasePath.'/data/country_name.sql');
140         $this->pgsqlRunScriptFile(CONST_BasePath.'/data/country_osm_grid.sql.gz');
141         $this->pgsqlRunScriptFile(CONST_BasePath.'/data/gb_postcode_table.sql');
142
143         $sPostcodeFilename = CONST_BasePath.'/data/gb_postcode_data.sql.gz';
144         if (file_exists($sPostcodeFilename)) {
145             $this->pgsqlRunScriptFile($sPostcodeFilename);
146         } else {
147             warn('optional external UK postcode table file ('.$sPostcodeFilename.') not found. Skipping.');
148         }
149
150         if (CONST_Use_Extra_US_Postcodes) {
151             $this->pgsqlRunScriptFile(CONST_BasePath.'/data/us_postcode.sql');
152         }
153
154         if ($this->bNoPartitions) {
155             $this->pgsqlRunScript('update country_name set partition = 0');
156         }
157
158         // the following will be needed by createFunctions later but
159         // is only defined in the subsequently called createTables
160         // Create dummies here that will be overwritten by the proper
161         // versions in create-tables.
162         $this->pgsqlRunScript('CREATE TABLE IF NOT EXISTS place_boundingbox ()');
163         $this->pgsqlRunScript('CREATE TYPE wikipedia_article_match AS ()', false);
164     }
165
166     public function importData($sOSMFile)
167     {
168         info('Import data');
169
170         $osm2pgsql = CONST_Osm2pgsql_Binary;
171         if (!file_exists($osm2pgsql)) {
172             echo "Check CONST_Osm2pgsql_Binary in your local settings file.\n";
173             echo "Normally you should not need to set this manually.\n";
174             fail("osm2pgsql not found in '$osm2pgsql'");
175         }
176
177         $osm2pgsql .= ' -S '.CONST_Import_Style;
178
179         if (!is_null(CONST_Osm2pgsql_Flatnode_File) && CONST_Osm2pgsql_Flatnode_File) {
180             $osm2pgsql .= ' --flat-nodes '.CONST_Osm2pgsql_Flatnode_File;
181         }
182
183         if (CONST_Tablespace_Osm2pgsql_Data)
184             $osm2pgsql .= ' --tablespace-slim-data '.CONST_Tablespace_Osm2pgsql_Data;
185         if (CONST_Tablespace_Osm2pgsql_Index)
186             $osm2pgsql .= ' --tablespace-slim-index '.CONST_Tablespace_Osm2pgsql_Index;
187         if (CONST_Tablespace_Place_Data)
188             $osm2pgsql .= ' --tablespace-main-data '.CONST_Tablespace_Place_Data;
189         if (CONST_Tablespace_Place_Index)
190             $osm2pgsql .= ' --tablespace-main-index '.CONST_Tablespace_Place_Index;
191         $osm2pgsql .= ' -lsc -O gazetteer --hstore --number-processes 1';
192         $osm2pgsql .= ' -C '.$this->iCacheMemory;
193         $osm2pgsql .= ' -P '.$this->aDSNInfo['port'];
194         if (isset($this->aDSNInfo['username'])) {
195             $osm2pgsql .= ' -U '.$this->aDSNInfo['username'];
196         }
197         if (isset($this->aDSNInfo['hostspec'])) {
198             $osm2pgsql .= ' -H '.$this->aDSNInfo['hostspec'];
199         }
200         $osm2pgsql .= ' -d '.$this->aDSNInfo['database'].' '.$sOSMFile;
201
202         $this->runWithPgEnv($osm2pgsql);
203
204         if (!$this->sIgnoreErrors && !$this->oDB->getRow('select * from place limit 1')) {
205             fail('No Data');
206         }
207     }
208
209     public function createFunctions()
210     {
211         info('Create Functions');
212
213         // Try accessing the C module, so we know early if something is wrong
214         checkModulePresence(); // raises exception on failure
215
216         $this->createSqlFunctions();
217     }
218
219     public function createTables($bReverseOnly = false)
220     {
221         info('Create Tables');
222
223         $sTemplate = file_get_contents(CONST_BasePath.'/sql/tables.sql');
224         $sTemplate = str_replace('{www-user}', CONST_Database_Web_User, $sTemplate);
225         $sTemplate = $this->replaceTablespace(
226             '{ts:address-data}',
227             CONST_Tablespace_Address_Data,
228             $sTemplate
229         );
230         $sTemplate = $this->replaceTablespace(
231             '{ts:address-index}',
232             CONST_Tablespace_Address_Index,
233             $sTemplate
234         );
235         $sTemplate = $this->replaceTablespace(
236             '{ts:search-data}',
237             CONST_Tablespace_Search_Data,
238             $sTemplate
239         );
240         $sTemplate = $this->replaceTablespace(
241             '{ts:search-index}',
242             CONST_Tablespace_Search_Index,
243             $sTemplate
244         );
245         $sTemplate = $this->replaceTablespace(
246             '{ts:aux-data}',
247             CONST_Tablespace_Aux_Data,
248             $sTemplate
249         );
250         $sTemplate = $this->replaceTablespace(
251             '{ts:aux-index}',
252             CONST_Tablespace_Aux_Index,
253             $sTemplate
254         );
255
256         $this->pgsqlRunScript($sTemplate, false);
257
258         if ($bReverseOnly) {
259             $this->pgExec('DROP TABLE search_name');
260         }
261
262         $oAlParser = new AddressLevelParser(CONST_Address_Level_Config);
263         $oAlParser->createTable($this->oDB, 'address_levels');
264     }
265
266     public function createPartitionTables()
267     {
268         info('Create Partition Tables');
269
270         $sTemplate = file_get_contents(CONST_BasePath.'/sql/partition-tables.src.sql');
271         $sTemplate = $this->replaceTablespace(
272             '{ts:address-data}',
273             CONST_Tablespace_Address_Data,
274             $sTemplate
275         );
276
277         $sTemplate = $this->replaceTablespace(
278             '{ts:address-index}',
279             CONST_Tablespace_Address_Index,
280             $sTemplate
281         );
282
283         $sTemplate = $this->replaceTablespace(
284             '{ts:search-data}',
285             CONST_Tablespace_Search_Data,
286             $sTemplate
287         );
288
289         $sTemplate = $this->replaceTablespace(
290             '{ts:search-index}',
291             CONST_Tablespace_Search_Index,
292             $sTemplate
293         );
294
295         $sTemplate = $this->replaceTablespace(
296             '{ts:aux-data}',
297             CONST_Tablespace_Aux_Data,
298             $sTemplate
299         );
300
301         $sTemplate = $this->replaceTablespace(
302             '{ts:aux-index}',
303             CONST_Tablespace_Aux_Index,
304             $sTemplate
305         );
306
307         $this->pgsqlRunPartitionScript($sTemplate);
308     }
309
310     public function createPartitionFunctions()
311     {
312         info('Create Partition Functions');
313
314         $sTemplate = file_get_contents(CONST_BasePath.'/sql/partition-functions.src.sql');
315         $this->pgsqlRunPartitionScript($sTemplate);
316     }
317
318     public function importWikipediaArticles()
319     {
320         $sWikiArticlesFile = CONST_Wikipedia_Data_Path.'/wikipedia_article.sql.bin';
321         $sWikiRedirectsFile = CONST_Wikipedia_Data_Path.'/wikipedia_redirect.sql.bin';
322         if (file_exists($sWikiArticlesFile)) {
323             info('Importing wikipedia articles');
324             $this->pgsqlRunDropAndRestore($sWikiArticlesFile);
325         } else {
326             warn('wikipedia article dump file not found - places will have default importance');
327         }
328         if (file_exists($sWikiRedirectsFile)) {
329             info('Importing wikipedia redirects');
330             $this->pgsqlRunDropAndRestore($sWikiRedirectsFile);
331         } else {
332             warn('wikipedia redirect dump file not found - some place importance values may be missing');
333         }
334     }
335
336     public function loadData($bDisableTokenPrecalc)
337     {
338         info('Drop old Data');
339
340         $this->pgExec('TRUNCATE word');
341         echo '.';
342         $this->pgExec('TRUNCATE placex');
343         echo '.';
344         $this->pgExec('TRUNCATE location_property_osmline');
345         echo '.';
346         $this->pgExec('TRUNCATE place_addressline');
347         echo '.';
348         $this->pgExec('TRUNCATE place_boundingbox');
349         echo '.';
350         $this->pgExec('TRUNCATE location_area');
351         echo '.';
352         if (!$this->dbReverseOnly()) {
353             $this->pgExec('TRUNCATE search_name');
354             echo '.';
355         }
356         $this->pgExec('TRUNCATE search_name_blank');
357         echo '.';
358         $this->pgExec('DROP SEQUENCE seq_place');
359         echo '.';
360         $this->pgExec('CREATE SEQUENCE seq_place start 100000');
361         echo '.';
362
363         $sSQL = 'select distinct partition from country_name';
364         $aPartitions = $this->oDB->getCol($sSQL);
365
366         if (!$this->bNoPartitions) $aPartitions[] = 0;
367         foreach ($aPartitions as $sPartition) {
368             $this->pgExec('TRUNCATE location_road_'.$sPartition);
369             echo '.';
370         }
371
372         // used by getorcreate_word_id to ignore frequent partial words
373         $sSQL = 'CREATE OR REPLACE FUNCTION get_maxwordfreq() RETURNS integer AS ';
374         $sSQL .= '$$ SELECT '.CONST_Max_Word_Frequency.' as maxwordfreq; $$ LANGUAGE SQL IMMUTABLE';
375         $this->pgExec($sSQL);
376         echo ".\n";
377
378         // pre-create the word list
379         if (!$bDisableTokenPrecalc) {
380             info('Loading word list');
381             $this->pgsqlRunScriptFile(CONST_BasePath.'/data/words.sql');
382         }
383
384         info('Load Data');
385         $sColumns = 'osm_type, osm_id, class, type, name, admin_level, address, extratags, geometry';
386
387         $aDBInstances = array();
388         $iLoadThreads = max(1, $this->iInstances - 1);
389         for ($i = 0; $i < $iLoadThreads; $i++) {
390             // https://secure.php.net/manual/en/function.pg-connect.php
391             $DSN = CONST_Database_DSN;
392             $DSN = preg_replace('/^pgsql:/', '', $DSN);
393             $DSN = preg_replace('/;/', ' ', $DSN);
394             $aDBInstances[$i] = pg_connect($DSN, PGSQL_CONNECT_FORCE_NEW);
395             pg_ping($aDBInstances[$i]);
396         }
397
398         for ($i = 0; $i < $iLoadThreads; $i++) {
399             $sSQL = "INSERT INTO placex ($sColumns) SELECT $sColumns FROM place WHERE osm_id % $iLoadThreads = $i";
400             $sSQL .= " and not (class='place' and type='houses' and osm_type='W'";
401             $sSQL .= "          and ST_GeometryType(geometry) = 'ST_LineString')";
402             $sSQL .= ' and ST_IsValid(geometry)';
403             if ($this->bVerbose) echo "$sSQL\n";
404             if (!pg_send_query($aDBInstances[$i], $sSQL)) {
405                 fail(pg_last_error($aDBInstances[$i]));
406             }
407         }
408
409         // last thread for interpolation lines
410         // https://secure.php.net/manual/en/function.pg-connect.php
411         $DSN = CONST_Database_DSN;
412         $DSN = preg_replace('/^pgsql:/', '', $DSN);
413         $DSN = preg_replace('/;/', ' ', $DSN);
414         $aDBInstances[$iLoadThreads] = pg_connect($DSN, PGSQL_CONNECT_FORCE_NEW);
415         pg_ping($aDBInstances[$iLoadThreads]);
416         $sSQL = 'insert into location_property_osmline';
417         $sSQL .= ' (osm_id, address, linegeo)';
418         $sSQL .= ' SELECT osm_id, address, geometry from place where ';
419         $sSQL .= "class='place' and type='houses' and osm_type='W' and ST_GeometryType(geometry) = 'ST_LineString'";
420         if ($this->bVerbose) echo "$sSQL\n";
421         if (!pg_send_query($aDBInstances[$iLoadThreads], $sSQL)) {
422             fail(pg_last_error($aDBInstances[$iLoadThreads]));
423         }
424
425         $bFailed = false;
426         for ($i = 0; $i <= $iLoadThreads; $i++) {
427             while (($hPGresult = pg_get_result($aDBInstances[$i])) !== false) {
428                 $resultStatus = pg_result_status($hPGresult);
429                 // PGSQL_EMPTY_QUERY, PGSQL_COMMAND_OK, PGSQL_TUPLES_OK,
430                 // PGSQL_COPY_OUT, PGSQL_COPY_IN, PGSQL_BAD_RESPONSE,
431                 // PGSQL_NONFATAL_ERROR and PGSQL_FATAL_ERROR
432                 // echo 'Query result ' . $i . ' is: ' . $resultStatus . "\n";
433                 if ($resultStatus != PGSQL_COMMAND_OK && $resultStatus != PGSQL_TUPLES_OK) {
434                     $resultError = pg_result_error($hPGresult);
435                     echo '-- error text ' . $i . ': ' . $resultError . "\n";
436                     $bFailed = true;
437                 }
438             }
439         }
440         if ($bFailed) {
441             fail('SQL errors loading placex and/or location_property_osmline tables');
442         }
443
444         for ($i = 0; $i < $this->iInstances; $i++) {
445             pg_close($aDBInstances[$i]);
446         }
447
448         echo "\n";
449         info('Reanalysing database');
450         $this->pgsqlRunScript('ANALYSE');
451
452         $sDatabaseDate = getDatabaseDate($this->oDB);
453         $this->oDB->exec('TRUNCATE import_status');
454         if (!$sDatabaseDate) {
455             warn('could not determine database date.');
456         } else {
457             $sSQL = "INSERT INTO import_status (lastimportdate) VALUES('".$sDatabaseDate."')";
458             $this->oDB->exec($sSQL);
459             echo "Latest data imported from $sDatabaseDate.\n";
460         }
461     }
462
463     public function importTigerData()
464     {
465         info('Import Tiger data');
466
467         $sTemplate = file_get_contents(CONST_BasePath.'/sql/tiger_import_start.sql');
468         $sTemplate = str_replace('{www-user}', CONST_Database_Web_User, $sTemplate);
469         $sTemplate = $this->replaceTablespace(
470             '{ts:aux-data}',
471             CONST_Tablespace_Aux_Data,
472             $sTemplate
473         );
474         $sTemplate = $this->replaceTablespace(
475             '{ts:aux-index}',
476             CONST_Tablespace_Aux_Index,
477             $sTemplate
478         );
479         $this->pgsqlRunScript($sTemplate, false);
480
481         $aDBInstances = array();
482         for ($i = 0; $i < $this->iInstances; $i++) {
483             // https://secure.php.net/manual/en/function.pg-connect.php
484             $DSN = CONST_Database_DSN;
485             $DSN = preg_replace('/^pgsql:/', '', $DSN);
486             $DSN = preg_replace('/;/', ' ', $DSN);
487             $aDBInstances[$i] = pg_connect($DSN, PGSQL_CONNECT_FORCE_NEW | PGSQL_CONNECT_ASYNC);
488             pg_ping($aDBInstances[$i]);
489         }
490
491         foreach (glob(CONST_Tiger_Data_Path.'/*.sql') as $sFile) {
492             echo $sFile.': ';
493             $hFile = fopen($sFile, 'r');
494             $sSQL = fgets($hFile, 100000);
495             $iLines = 0;
496             while (true) {
497                 for ($i = 0; $i < $this->iInstances; $i++) {
498                     if (!pg_connection_busy($aDBInstances[$i])) {
499                         while (pg_get_result($aDBInstances[$i]));
500                         $sSQL = fgets($hFile, 100000);
501                         if (!$sSQL) break 2;
502                         if (!pg_send_query($aDBInstances[$i], $sSQL)) fail(pg_last_error($aDBInstances[$i]));
503                         $iLines++;
504                         if ($iLines == 1000) {
505                             echo '.';
506                             $iLines = 0;
507                         }
508                     }
509                 }
510                 usleep(10);
511             }
512             fclose($hFile);
513
514             $bAnyBusy = true;
515             while ($bAnyBusy) {
516                 $bAnyBusy = false;
517                 for ($i = 0; $i < $this->iInstances; $i++) {
518                     if (pg_connection_busy($aDBInstances[$i])) $bAnyBusy = true;
519                 }
520                 usleep(10);
521             }
522             echo "\n";
523         }
524
525         for ($i = 0; $i < $this->iInstances; $i++) {
526             pg_close($aDBInstances[$i]);
527         }
528
529         info('Creating indexes on Tiger data');
530         $sTemplate = file_get_contents(CONST_BasePath.'/sql/tiger_import_finish.sql');
531         $sTemplate = str_replace('{www-user}', CONST_Database_Web_User, $sTemplate);
532         $sTemplate = $this->replaceTablespace(
533             '{ts:aux-data}',
534             CONST_Tablespace_Aux_Data,
535             $sTemplate
536         );
537         $sTemplate = $this->replaceTablespace(
538             '{ts:aux-index}',
539             CONST_Tablespace_Aux_Index,
540             $sTemplate
541         );
542         $this->pgsqlRunScript($sTemplate, false);
543     }
544
545     public function calculatePostcodes($bCMDResultAll)
546     {
547         info('Calculate Postcodes');
548         $this->pgExec('TRUNCATE location_postcode');
549
550         $sSQL  = 'INSERT INTO location_postcode';
551         $sSQL .= ' (place_id, indexed_status, country_code, postcode, geometry) ';
552         $sSQL .= "SELECT nextval('seq_place'), 1, country_code,";
553         $sSQL .= "       upper(trim (both ' ' from address->'postcode')) as pc,";
554         $sSQL .= '       ST_Centroid(ST_Collect(ST_Centroid(geometry)))';
555         $sSQL .= '  FROM placex';
556         $sSQL .= " WHERE address ? 'postcode' AND address->'postcode' NOT SIMILAR TO '%(,|;)%'";
557         $sSQL .= '       AND geometry IS NOT null';
558         $sSQL .= ' GROUP BY country_code, pc';
559         $this->pgExec($sSQL);
560
561         if (CONST_Use_Extra_US_Postcodes) {
562             // only add postcodes that are not yet available in OSM
563             $sSQL  = 'INSERT INTO location_postcode';
564             $sSQL .= ' (place_id, indexed_status, country_code, postcode, geometry) ';
565             $sSQL .= "SELECT nextval('seq_place'), 1, 'us', postcode,";
566             $sSQL .= '       ST_SetSRID(ST_Point(x,y),4326)';
567             $sSQL .= '  FROM us_postcode WHERE postcode NOT IN';
568             $sSQL .= '        (SELECT postcode FROM location_postcode';
569             $sSQL .= "          WHERE country_code = 'us')";
570             $this->pgExec($sSQL);
571         }
572
573         // add missing postcodes for GB (if available)
574         $sSQL  = 'INSERT INTO location_postcode';
575         $sSQL .= ' (place_id, indexed_status, country_code, postcode, geometry) ';
576         $sSQL .= "SELECT nextval('seq_place'), 1, 'gb', postcode, geometry";
577         $sSQL .= '  FROM gb_postcode WHERE postcode NOT IN';
578         $sSQL .= '           (SELECT postcode FROM location_postcode';
579         $sSQL .= "             WHERE country_code = 'gb')";
580         $this->pgExec($sSQL);
581
582         if (!$bCMDResultAll) {
583             $sSQL = "DELETE FROM word WHERE class='place' and type='postcode'";
584             $sSQL .= 'and word NOT IN (SELECT postcode FROM location_postcode)';
585             $this->pgExec($sSQL);
586         }
587
588         $sSQL = 'SELECT count(getorcreate_postcode_id(v)) FROM ';
589         $sSQL .= '(SELECT distinct(postcode) as v FROM location_postcode) p';
590         $this->pgExec($sSQL);
591     }
592
593     public function index($bIndexNoanalyse)
594     {
595         $sOutputFile = '';
596         $sBaseCmd = CONST_InstallPath.'/nominatim/nominatim -i -d '.$this->aDSNInfo['database'].' -P '
597             .$this->aDSNInfo['port'].' -t '.$this->iInstances.$sOutputFile;
598         if (isset($this->aDSNInfo['hostspec'])) {
599             $sBaseCmd .= ' -H '.$this->aDSNInfo['hostspec'];
600         }
601         if (isset($this->aDSNInfo['username'])) {
602             $sBaseCmd .= ' -U '.$this->aDSNInfo['username'];
603         }
604
605         info('Index ranks 0 - 4');
606         $iStatus = $this->runWithPgEnv($sBaseCmd.' -R 4');
607         if ($iStatus != 0) {
608             fail('error status ' . $iStatus . ' running nominatim!');
609         }
610         if (!$bIndexNoanalyse) $this->pgsqlRunScript('ANALYSE');
611
612         info('Index ranks 5 - 25');
613         $iStatus = $this->runWithPgEnv($sBaseCmd.' -r 5 -R 25');
614         if ($iStatus != 0) {
615             fail('error status ' . $iStatus . ' running nominatim!');
616         }
617         if (!$bIndexNoanalyse) $this->pgsqlRunScript('ANALYSE');
618
619         info('Index ranks 26 - 30');
620         $iStatus = $this->runWithPgEnv($sBaseCmd.' -r 26');
621         if ($iStatus != 0) {
622             fail('error status ' . $iStatus . ' running nominatim!');
623         }
624
625         info('Index postcodes');
626         $sSQL = 'UPDATE location_postcode SET indexed_status = 0';
627         $this->pgExec($sSQL);
628     }
629
630     public function createSearchIndices()
631     {
632         info('Create Search indices');
633
634         $sTemplate = file_get_contents(CONST_BasePath.'/sql/indices.src.sql');
635         if (!$this->dbReverseOnly()) {
636             $sTemplate .= file_get_contents(CONST_BasePath.'/sql/indices_search.src.sql');
637         }
638         $sTemplate = str_replace('{www-user}', CONST_Database_Web_User, $sTemplate);
639         $sTemplate = $this->replaceTablespace(
640             '{ts:address-index}',
641             CONST_Tablespace_Address_Index,
642             $sTemplate
643         );
644         $sTemplate = $this->replaceTablespace(
645             '{ts:search-index}',
646             CONST_Tablespace_Search_Index,
647             $sTemplate
648         );
649         $sTemplate = $this->replaceTablespace(
650             '{ts:aux-index}',
651             CONST_Tablespace_Aux_Index,
652             $sTemplate
653         );
654         $this->pgsqlRunScript($sTemplate);
655     }
656
657     public function createCountryNames()
658     {
659         info('Create search index for default country names');
660
661         $this->pgsqlRunScript("select getorcreate_country(make_standard_name('uk'), 'gb')");
662         $this->pgsqlRunScript("select getorcreate_country(make_standard_name('united states'), 'us')");
663         $this->pgsqlRunScript('select count(*) from (select getorcreate_country(make_standard_name(country_code), country_code) from country_name where country_code is not null) as x');
664         $this->pgsqlRunScript("select count(*) from (select getorcreate_country(make_standard_name(name->'name'), country_code) from country_name where name ? 'name') as x");
665         $sSQL = 'select count(*) from (select getorcreate_country(make_standard_name(v),'
666             .'country_code) from (select country_code, skeys(name) as k, svals(name) as v from country_name) x where k ';
667         if (CONST_Languages) {
668             $sSQL .= 'in ';
669             $sDelim = '(';
670             foreach (explode(',', CONST_Languages) as $sLang) {
671                 $sSQL .= $sDelim."'name:$sLang'";
672                 $sDelim = ',';
673             }
674             $sSQL .= ')';
675         } else {
676             // all include all simple name tags
677             $sSQL .= "like 'name:%'";
678         }
679         $sSQL .= ') v';
680         $this->pgsqlRunScript($sSQL);
681     }
682
683     public function drop()
684     {
685         info('Drop tables only required for updates');
686
687         // The implementation is potentially a bit dangerous because it uses
688         // a positive selection of tables to keep, and deletes everything else.
689         // Including any tables that the unsuspecting user might have manually
690         // created. USE AT YOUR OWN PERIL.
691         // tables we want to keep. everything else goes.
692         $aKeepTables = array(
693                         '*columns',
694                         'import_polygon_*',
695                         'import_status',
696                         'place_addressline',
697                         'location_postcode',
698                         'location_property*',
699                         'placex',
700                         'search_name',
701                         'seq_*',
702                         'word',
703                         'query_log',
704                         'new_query_log',
705                         'spatial_ref_sys',
706                         'country_name',
707                         'place_classtype_*',
708                         'country_osm_grid'
709                        );
710
711         $aDropTables = array();
712         $aHaveTables = $this->oDB->getCol("SELECT tablename FROM pg_tables WHERE schemaname='public'");
713
714         foreach ($aHaveTables as $sTable) {
715             $bFound = false;
716             foreach ($aKeepTables as $sKeep) {
717                 if (fnmatch($sKeep, $sTable)) {
718                     $bFound = true;
719                     break;
720                 }
721             }
722             if (!$bFound) array_push($aDropTables, $sTable);
723         }
724         foreach ($aDropTables as $sDrop) {
725             if ($this->bVerbose) echo "Dropping table $sDrop\n";
726             $this->oDB->exec("DROP TABLE $sDrop CASCADE");
727             // ignore warnings/errors as they might be caused by a table having
728             // been deleted already by CASCADE
729         }
730
731         if (!is_null(CONST_Osm2pgsql_Flatnode_File) && CONST_Osm2pgsql_Flatnode_File) {
732             if (file_exists(CONST_Osm2pgsql_Flatnode_File)) {
733                 if ($this->bVerbose) echo 'Deleting '.CONST_Osm2pgsql_Flatnode_File."\n";
734                 unlink(CONST_Osm2pgsql_Flatnode_File);
735             }
736         }
737     }
738
739     private function pgsqlRunDropAndRestore($sDumpFile)
740     {
741         $sCMD = 'pg_restore -p '.$this->aDSNInfo['port'].' -d '.$this->aDSNInfo['database'].' --no-owner -Fc --clean '.$sDumpFile;
742         if ($this->oDB->getPostgresVersion() >= 9.04) {
743             $sCMD .= ' --if-exists';
744         }
745         if (isset($this->aDSNInfo['hostspec'])) {
746             $sCMD .= ' -h '.$this->aDSNInfo['hostspec'];
747         }
748         if (isset($this->aDSNInfo['username'])) {
749             $sCMD .= ' -U '.$this->aDSNInfo['username'];
750         }
751
752         $this->runWithPgEnv($sCMD);
753     }
754
755     private function pgsqlRunScript($sScript, $bfatal = true)
756     {
757         runSQLScript(
758             $sScript,
759             $bfatal,
760             $this->bVerbose,
761             $this->sIgnoreErrors
762         );
763     }
764
765     private function createSqlFunctions()
766     {
767         $sTemplate = file_get_contents(CONST_BasePath.'/sql/functions.sql');
768         $sTemplate = str_replace('{modulepath}', $this->sModulePath, $sTemplate);
769         if ($this->bEnableDiffUpdates) {
770             $sTemplate = str_replace('RETURN NEW; -- %DIFFUPDATES%', '--', $sTemplate);
771         }
772         if ($this->bEnableDebugStatements) {
773             $sTemplate = str_replace('--DEBUG:', '', $sTemplate);
774         }
775         if (CONST_Limit_Reindexing) {
776             $sTemplate = str_replace('--LIMIT INDEXING:', '', $sTemplate);
777         }
778         if (!CONST_Use_US_Tiger_Data) {
779             $sTemplate = str_replace('-- %NOTIGERDATA% ', '', $sTemplate);
780         }
781         if (!CONST_Use_Aux_Location_data) {
782             $sTemplate = str_replace('-- %NOAUXDATA% ', '', $sTemplate);
783         }
784
785         $sReverseOnly = $this->dbReverseOnly() ? 'true' : 'false';
786         $sTemplate = str_replace('%REVERSE-ONLY%', $sReverseOnly, $sTemplate);
787
788         $this->pgsqlRunScript($sTemplate);
789     }
790
791     private function pgsqlRunPartitionScript($sTemplate)
792     {
793         $sSQL = 'select distinct partition from country_name';
794         $aPartitions = $this->oDB->getCol($sSQL);
795         if (!$this->bNoPartitions) $aPartitions[] = 0;
796
797         preg_match_all('#^-- start(.*?)^-- end#ms', $sTemplate, $aMatches, PREG_SET_ORDER);
798         foreach ($aMatches as $aMatch) {
799             $sResult = '';
800             foreach ($aPartitions as $sPartitionName) {
801                 $sResult .= str_replace('-partition-', $sPartitionName, $aMatch[1]);
802             }
803             $sTemplate = str_replace($aMatch[0], $sResult, $sTemplate);
804         }
805
806         $this->pgsqlRunScript($sTemplate);
807     }
808
809     private function pgsqlRunScriptFile($sFilename)
810     {
811         if (!file_exists($sFilename)) fail('unable to find '.$sFilename);
812
813         $sCMD = 'psql -p '.$this->aDSNInfo['port'].' -d '.$this->aDSNInfo['database'];
814         if (!$this->bVerbose) {
815             $sCMD .= ' -q';
816         }
817         if (isset($this->aDSNInfo['hostspec'])) {
818             $sCMD .= ' -h '.$this->aDSNInfo['hostspec'];
819         }
820         if (isset($this->aDSNInfo['username'])) {
821             $sCMD .= ' -U '.$this->aDSNInfo['username'];
822         }
823         $aProcEnv = null;
824         if (isset($this->aDSNInfo['password'])) {
825             $aProcEnv = array_merge(array('PGPASSWORD' => $this->aDSNInfo['password']), $_ENV);
826         }
827         $ahGzipPipes = null;
828         if (preg_match('/\\.gz$/', $sFilename)) {
829             $aDescriptors = array(
830                              0 => array('pipe', 'r'),
831                              1 => array('pipe', 'w'),
832                              2 => array('file', '/dev/null', 'a')
833                             );
834             $hGzipProcess = proc_open('zcat '.$sFilename, $aDescriptors, $ahGzipPipes);
835             if (!is_resource($hGzipProcess)) fail('unable to start zcat');
836             $aReadPipe = $ahGzipPipes[1];
837             fclose($ahGzipPipes[0]);
838         } else {
839             $sCMD .= ' -f '.$sFilename;
840             $aReadPipe = array('pipe', 'r');
841         }
842         $aDescriptors = array(
843                          0 => $aReadPipe,
844                          1 => array('pipe', 'w'),
845                          2 => array('file', '/dev/null', 'a')
846                         );
847         $ahPipes = null;
848         $hProcess = proc_open($sCMD, $aDescriptors, $ahPipes, null, $aProcEnv);
849         if (!is_resource($hProcess)) fail('unable to start pgsql');
850         // TODO: error checking
851         while (!feof($ahPipes[1])) {
852             echo fread($ahPipes[1], 4096);
853         }
854         fclose($ahPipes[1]);
855         $iReturn = proc_close($hProcess);
856         if ($iReturn > 0) {
857             fail("pgsql returned with error code ($iReturn)");
858         }
859         if ($ahGzipPipes) {
860             fclose($ahGzipPipes[1]);
861             proc_close($hGzipProcess);
862         }
863     }
864
865     private function replaceTablespace($sTemplate, $sTablespace, $sSql)
866     {
867         if ($sTablespace) {
868             $sSql = str_replace($sTemplate, 'TABLESPACE "'.$sTablespace.'"', $sSql);
869         } else {
870             $sSql = str_replace($sTemplate, '', $sSql);
871         }
872         return $sSql;
873     }
874
875     private function runWithPgEnv($sCmd)
876     {
877         if ($this->bVerbose) {
878             echo "Execute: $sCmd\n";
879         }
880
881         $aProcEnv = null;
882
883         if (isset($this->aDSNInfo['password'])) {
884             $aProcEnv = array_merge(array('PGPASSWORD' => $this->aDSNInfo['password']), $_ENV);
885         }
886
887         return runWithEnv($sCmd, $aProcEnv);
888     }
889
890     /**
891      * Execute the SQL command on the open database.
892      *
893      * @param string $sSQL SQL command to execute.
894      *
895      * @return null
896      *
897      * @pre connect() must have been called.
898      */
899     private function pgExec($sSQL)
900     {
901         $this->oDB->exec($sSQL);
902     }
903
904     /**
905      * Check if the database is in reverse-only mode.
906      *
907      * @return True if there is no search_name table and infrastructure.
908      */
909     private function dbReverseOnly()
910     {
911         return !($this->oDB->tableExists('search_name'));
912     }
913 }