From 30e9a84563a15ab70d2250f902c90ab27d4f6e04 Mon Sep 17 00:00:00 2001 From: Thang Le Quoc Date: Sun, 12 Jul 2026 23:26:57 +0700 Subject: [PATCH 1/4] fix: correct .env.example Postgres port from 3306 to 15432 --- dataset-generation-scripts/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dataset-generation-scripts/.env.example b/dataset-generation-scripts/.env.example index 82285a76..452bfbb2 100644 --- a/dataset-generation-scripts/.env.example +++ b/dataset-generation-scripts/.env.example @@ -1,5 +1,5 @@ POSTGRES_DB_USERNAME=db_user_name POSTGRES_DB_PSWD=db_user_pswd POSTGRES_DB_HOST=localhost -POSTGRES_DB_PORT=3306 +POSTGRES_DB_PORT=15432 POSTGRES_TMP_DB_NAME=vn_provinces_tmp From 907f1744a7df0d9446824fe7f9b7c33463aac104 Mon Sep 17 00:00:00 2001 From: Thang Le Quoc Date: Sun, 12 Jul 2026 23:27:19 +0700 Subject: [PATCH 2/4] chore: add .env to .gitignore to prevent credential commits --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e94ff858..aa86358a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .vscode dataset-generation-scripts/output/* +dataset-generation-scripts/.env From 604878324f406bee978164c7f46fd93c136a9ff6 Mon Sep 17 00:00:00 2001 From: Thang Le Quoc Date: Sun, 12 Jul 2026 23:28:32 +0700 Subject: [PATCH 3/4] docs: rewrite README as complete setup guide with prerequisites, verify steps, and troubleshooting --- dataset-generation-scripts/README.md | 152 +++++++++++++++++++-------- 1 file changed, 108 insertions(+), 44 deletions(-) diff --git a/dataset-generation-scripts/README.md b/dataset-generation-scripts/README.md index 256736a6..0d0251b1 100644 --- a/dataset-generation-scripts/README.md +++ b/dataset-generation-scripts/README.md @@ -1,64 +1,128 @@ -> ⚠️ **Heads-up!** -This section is only useful for the repository maintainers. Feel free to ignore this section if you are just looking for the Vietnamese Database Dataset. +> ⚠️ **Heads-up!** +> This section is for maintainers who need to regenerate the dataset. If you're looking to use the dataset, see the [root README](../README.md). -# Vietnames Provinces Database Dataset Automation Scripts +# Vietnamese Provinces Database Dataset Automation Scripts -Vietnamese Government will gradually issue new decree to make change to the administrative units. The change is vary: promoting ward to a higher administrative unit, merging ward, etc... -Therefore, it's challenging to keep the dataset up-to-date. Before the development of this automation scripts, I have to manually read the decree change and compare it with the new xls sheet (which is kind of dumb and counter-productive way 🙈) -Then it finally come to the development of this autogenerated patch script to make this repository maintainers's life easy. +The Vietnamese Government periodically issues decrees that change administrative units — merging wards, promoting them to higher units, etc. These automation scripts ingest the latest administrative data, enrich it with GIS geometry, and generate import scripts across multiple database formats. -## How does it work -[![image.png](https://i.postimg.cc/BnY9f29C/image.png)](https://postimg.cc/dhyS8k07) +## How it works -The scripts are divided into two main phases +[![architecture diagram](https://i.postimg.cc/BnY9f29C/image.png)](https://postimg.cc/dhyS8k07) -- **Dumper** (`internal/dumper/`): Reads administrative data from the DVHCVN SOAP source (or manual seeds), performs data transformation and inserts the records into a temporary Postgres database -- **Dataset Writer** (`internal/dataset_writer/`): Reads from the persisted temporary Postgres database and generates import scripts for multiple databases (PostgreSQL/MySQL, SQL Server, Oracle), plus JSON, MongoDB, and Redis exports +The scripts operate in two phases: + +- **Dumper** (`internal/dumper/`): Reads administrative data from the DVHCVN SOAP source, transforms the records, and inserts them into a temporary Postgres database +- **Dataset Writer** (`internal/dataset_writer/`): Reads from the temporary Postgres database and generates import scripts for multiple databases (PostgreSQL/MySQL, SQL Server, Oracle), plus JSON, MongoDB, and Redis exports See [CLAUDE.md](CLAUDE.md) for detailed subsystem context. -## How to run -### Prerequisite -You must have these thing installed on your machine -- Postgresql -- Go -### Setting thing up -#### Postgres temporary database -The easiest way is to use the Docker Postgres/PostGIS container: - -```shell + +## Prerequisites + +- **Go 1.24+** (matches `go.mod`) +- **Docker** (for the temporary Postgres/PostGIS database) + +All required data files (including GeoJSON geometry) are committed in the repository — no extra downloads or extraction needed. + +## Setup + +### 1. Clone the repository + +```bash +git clone git@github.com:ThangLeQuoc/vietnamese-provinces-database.git +cd vietnamese-provinces-database +``` + +### 2. Start the temporary Postgres database + +```bash +cd dataset-generation-scripts docker compose -f docker/docker-compose.yaml up -d ``` -This starts a Postgres/PostGIS container on port `15432` with database `vn_provinces_tmp`. +This starts a Postgres/PostGIS container named `vn_provinces_postgres_container` on port `15432` with database `vn_provinces_tmp`. + +### 3. Configure the `.env` file + +```bash +cp .env.example .env +``` + +The default values in `.env.example` match the Docker container — no edits needed for the standard setup. If you customized the Docker port or credentials, update `.env` accordingly. -Database credentials are configured via a `.env` file (see `.env.example` for the template). The connection is built programmatically in `internal/database/postgres_connector.go` from these environment variables: +## Run + +```bash +go run main.go +``` + +Results land in the `output/` directory. By default, the script generates: + +- SQL import scripts for PostgreSQL/MySQL, SQL Server, Oracle +- JSON, MongoDB, and Redis exports +- GIS SQL scripts and GeoJSON files + +**Skipping GIS**: The `INCLUDE_GIS` constant in `main.go` defaults to `true`. Set it to `false` for a faster, admin-only run that skips GIS data fetching and geometry output — no internet connection required. + +## Output structure + +After a successful run, the `output/` directory contains: ``` -POSTGRES_DB_USERNAME=postgres -POSTGRES_DB_PSWD=root -POSTGRES_DB_HOST=localhost -POSTGRES_DB_PORT=15432 -POSTGRES_TMP_DB_NAME=vn_provinces_tmp +output/ +├── postgresql_mysql_generated_ImportData_vn_units_*.sql # PostgreSQL & MySQL import +├── mssql_generated_ImportData_vn_units_*.sql # SQL Server import +├── oracle_generated_ImportData_vn_units_*.sql # Oracle import +├── json/ +│ ├── full_json_generated_data_vn_units_*.json # Full dataset (provinces + wards + districts) +│ ├── simplified_json_generated_data_vn_units_*.json # Simplified names +│ └── vn_only_simplified_json_generated_data_vn_units_*.json # Vietnamese-only simplified +├── mongodb/ +│ ├── administrative_regions_*.json +│ ├── administrative_units_*.json +│ └── mongo_data_vn_unit_*.json # Full MongoDB import +├── redis/ +│ └── redis_vn_provinces_dataset_*.redis # Redis commands +└── gis/ # (only if INCLUDE_GIS=true) + ├── *_ImportData_gis_*.sql # GIS SQL imports per engine + ├── *_ImportData_gis_*.sql.zip # Compressed versions + ├── vn_provinces_wards_geojson_*.zip # Combined GeoJSON archive + └── geojson/ # Per-province GeoJSON + ├── README.md + ├── 01_ha_noi/ + │ ├── 01_ha_noi.geojson # Province boundary + │ └── wards/ # Per-ward boundaries + │ ├── 00004_ba_dinh.geojson + │ └── ... + ├── 04_cao_bang/ + └── ... ``` -Copy `.env.example` to `.env` and adjust the values for your local setup. +## Verify success + +Check the expected record counts in the temporary database: -#### Git LFS -⚠️ This project uses [Git LFS](https://git-lfs.github.com/) for large GIS dataset files. -Make sure to run: -```shell -git lfs install -git lfs pull -git lfs fetch --all +```bash +docker exec vn_provinces_postgres_container psql -U postgres -d vn_provinces_tmp \ + -c "SELECT COUNT(*) FROM provinces_tmp; SELECT COUNT(*) FROM wards_tmp;" ``` -before running the project. -#### Executing -At the root of the `dataset-generation-scripts` folder, run -```shell -go run main.go +Expected: 34 provinces, 3,321 wards _(counts may change with new government decrees)_. + +## Tests + +```bash +# From dataset-generation-scripts/ +go test -v ./... ``` -And check the result in the `output` folder. The final result includes SQL import scripts for multiple database engines (PostgreSQL/MySQL, SQL Server, Oracle), plus JSON, MongoDB, and Redis exports. If `INCLUDE_GIS` is enabled in `main.go`, GIS SQL and GeoJSON files are also generated. -# Run testcase -> go test -v ./... +**Important**: Docker must be running — most tests connect to the temporary Postgres database. Packages that require the database include `internal/sapnhap_bando/...` and `internal/dumper/...`. Pure unit tests (e.g., `internal/common/viet/`) will pass without Docker. + +## Troubleshooting + +| Symptom | Likely cause | Fix | +|---------|-------------|-----| +| `connect: connection refused` on port 15432 | Docker not running | `docker compose -f docker/docker-compose.yaml up -d` | +| Port 15432 already in use | Another Postgres instance on that port | Change the host port in `docker/docker-compose.yaml` and update `POSTGRES_DB_PORT` in `.env` | +| GIS pipeline errors or missing geometry | GeoJSON files missing from `resources/gis/geojson_11Mar2026/` | Ensure the repository was cloned correctly; the GeoJSON files are committed in Git | +| `package ... is not in GOROOT` | Go module dependencies not downloaded | Run `go mod download` from `dataset-generation-scripts/` | +| Tests fail with database connection errors | Database container not started | Verify with `docker ps`, check `.env` values match the Docker config | \ No newline at end of file From 4e778d4f89e78954a1fdb6402baccdbbc1061061 Mon Sep 17 00:00:00 2001 From: Thang Le Quoc Date: Sat, 18 Jul 2026 15:01:40 +0700 Subject: [PATCH 4/4] feat: Update doc --- dataset-generation-scripts/draft.md | 68 ------- .../167_PatchGISPolygonOfProvince/design.md | 184 ++++++++++++++++++ docs/gis/gis_example_query.md | 58 ++++++ 3 files changed, 242 insertions(+), 68 deletions(-) delete mode 100644 dataset-generation-scripts/draft.md create mode 100644 development/167_PatchGISPolygonOfProvince/design.md diff --git a/dataset-generation-scripts/draft.md b/dataset-generation-scripts/draft.md deleted file mode 100644 index ef2ea4ad..00000000 --- a/dataset-generation-scripts/draft.md +++ /dev/null @@ -1,68 +0,0 @@ -# Improvement plan - -- Revise the script execution -- Introduce unit testing? -- - -## Compare existing dataset and new dataset -- Compare strategy -- Pagination on the existing dataset, get a batch of n items. -- Query on the new dataset & compare the data. -- If the record does not found in the dataset -> unit has been remove (merge to another unit) -- There's a very rare chance that a new unit is introduced? How to prevent this? - -## Integration Test? -https://www.ardanlabs.com/blog/2019/03/integration-testing-in-go-executing-tests-with-docker.html - - - - --------------------------- - -Observe change: - -Remove district 358: Huyen My Loc - -Update Nam Phong - -Mỹ Hưng 13729 + Mỹ Lộc 13708 = Hưng Lộc 13708 - -Lộc An 13702 + Văn Miếu 13675 = Trương Thi (13657) - -Hạ Long 13633 + Thống Nhất 13681 = Quang Trung - -Trần Tế Xương 13636 + Vị Hoàng 13639 = Vị Xuyên - -Phan Đình Phùng 13660 + Nguyễn Du 13651 = Trần Hưng Đạo - -Ngô Quyền 13663 + Trần Quang Khải 13678 = Năng Tĩnh 13672 - -Bà Triệu 13654 + Trần Đăng Ninh 13669 = Cửa Bắc (13669) - -Mỹ Thành (13738) -> Mỹ Lộc (13735) - - - ---- Vụ Bản -Tân Khánh + Minh Thuận -> Minh Tân - -Tân Thành + Liên Bảo -> Thành Lợi - -Yên Thành + Yên Nghĩa + Yên Trung -> Xã Trung Nghĩa - -Yên Hưng + Yên Phú + Yên Phương -> Phú Hưng - -Yên Minh + Yên Lợi + Yên Tân -> Tân Minh - -Yên Quang + Yên Hồng + Yên Bằng -> Hồng Quang - ---- Nam Trực -Nam Toàn + Nam Mỹ + Điền Xá -> Nam Điền - --- Xuân Trường -Xuân Đài + Xuân Phong + Xuân Thuỷ -> Xuân Giang -Xuân Hoà + Xuân Kiên + Xuân Tiến -> Xuân Phúc - -Xuân Bắc + - -===== diff --git a/development/167_PatchGISPolygonOfProvince/design.md b/development/167_PatchGISPolygonOfProvince/design.md new file mode 100644 index 00000000..39624549 --- /dev/null +++ b/development/167_PatchGISPolygonOfProvince/design.md @@ -0,0 +1,184 @@ +# Design: Patch GIS Polygon of Province — Merge Island Territories + +## Problem + +The province-level GIS boundaries for **Da Nang (province_code = 48)** and **Khanh Hoa (province_code = 56)** do not include their respective island territories: + +- **Hoàng Sa (Paracel Islands, ward_code = 20333)** — belongs to Da Nang +- **Trường Sa (Spratly Islands, ward_code = 22736)** — belongs to Khanh Hoa + +The ward-level geometries are correctly fetched from the `sapnhap.bando.com.vn` API and stored in `sapnhap_geojson_objects`. However, the province-level API response excludes the island polygons entirely. + +### Evidence (from database query) + +| Province | Province bbox | Island ward | Island bbox | `ST_Contains` | `ST_Intersects` | +|----------|--------------|-------------|-------------|---------------|-----------------| +| Da Nang (ma=48) | `[107.21, 14.95, 108.74, 16.23]` | Hoàng Sa (ma=20333) | `[111.19, 15.69, 112.74, 17.12]` | `false` | `false` | +| Khanh Hoa (ma=56) | `[108.55, 11.31, 109.46, 12.87]` | Trường Sa (ma=22736) | `[109.47, 7.18, 117.83, 11.53]` | `false` | `false` | + +All other wards in both provinces spatially intersect their parent province polygon (93/94 for Da Nang, 64/65 for Khanh Hoa). The only wards that don't intersect at all are the two island territories — confirming this is an upstream GIS data defect, not a boundary precision issue. + +### Root Cause + +The upstream `sapnhap.bando.com.vn` API returns province geometries that exclude island territories. The ward-level geometries for the islands are fetched via separate API calls (using their own `malk` IDs) and are correct. The province-level API response simply doesn't include those polygons. + +## Approach: Post-Fetch SQL Patch Step (Approach A) + +Add a new step in the generation flow, executed **after** `FetchGISDataFromSapNhapBando()` and **before** `GenerateGISSQLDatasets()`. This step merges the island ward geometries into their parent province geometries using PostGIS `ST_Union`. + +### Why this approach? + +- **Leverages existing data:** The island ward geometries are already in the database from the API fetch — no additional API calls or local files needed +- **Clean separation:** The patch is a distinct step, easy to audit and reason about +- **Automatic:** Integrated into the generation flow, so the fix is applied every time the dataset is regenerated +- **Follows existing patterns:** Similar to how An Giang has a special-case patch in `processGeoJSONObject()`, but cleaner because it's a separate step rather than inline logic +- **PostGIS-native:** Uses `ST_Union` and `ST_Envelope` which are the correct tools for geometry merging and bbox recalculation + +### Why not the alternatives? + +- **Approach B (Go code special-case in `processGeoJSONObject`):** Would require either Go-level geometry manipulation (complex, no Go GIS library in project) or a DB round-trip mid-processing. Mixes concerns — the fetch step should fetch, the patch step should patch. +- **Approach C (standalone SQL patch script):** Not integrated into the automated generation flow. Users would need to manually run it, and generated datasets wouldn't automatically reflect the fix. + +## Implementation + +### 1. New Go function to execute the patch + +**File:** `dataset-generation-scripts/internal/sapnhap_bando/service/sapnhap.go` + +Add a new method to `SapNhapService`: + +```go +// PatchIslandProvincesGeometry merges island ward geometries into their parent +// province geometries. This fixes an upstream GIS data defect where the province-level +// API response from sapnhap.bando.com.vn excludes island territories (Hoàng Sa and +// Trường Sa) that are present at the ward level. +// +// Affected provinces: +// - Da Nang (ma=48) ← Hoàng Sa (ma=20333) +// - Khanh Hoa (ma=56) ← Trường Sa (ma=22736) +func (s *SapNhapService) PatchIslandProvincesGeometry(ctx context.Context) error { + // Merge Hoàng Sa into Da Nang + err := s.mergeWardGeometryIntoProvince(ctx, "48", "20333") + if err != nil { + return fmt.Errorf("failed to patch Da Nang with Hoàng Sa geometry: %w", err) + } + log.Println("✅ Patched Da Nang (48) with Hoàng Sa (20333) island geometry") + + // Merge Trường Sa into Khanh Hoa + err = s.mergeWardGeometryIntoProvince(ctx, "56", "22736") + if err != nil { + return fmt.Errorf("failed to patch Khanh Hoa with Trường Sa geometry: %w", err) + } + log.Println("✅ Patched Khanh Hoa (56) with Trường Sa (22736) island geometry") + + return nil +} + +// mergeWardGeometryIntoProvince merges the geometry of a ward (identified by wardMa) +// into the geometry of a province (identified by provinceMa) using ST_Union. +// Both geom_wkt and bbox_wkt are updated for the province record. +func (s *SapNhapService) mergeWardGeometryIntoProvince(ctx context.Context, provinceMa, wardMa string) error { + _, err := s.db.ExecContext(ctx, ` + UPDATE sapnhap_geojson_objects + SET geom_wkt = ST_AsText(ST_Union(geom, (SELECT geom FROM sapnhap_geojson_objects WHERE ma = ?))), + bbox_wkt = ST_AsText(ST_Envelope(ST_Union(geom, (SELECT geom FROM sapnhap_geojson_objects WHERE ma = ?)))) + WHERE ma = ?`, + wardMa, wardMa, provinceMa) + return err +} +``` + +### 2. New entry point function + +**File:** `dataset-generation-scripts/internal/sapnhap_bando/sapnhap_bando.go` + +Add a new exported function: + +```go +// PatchIslandProvincesGeometry merges island ward geometries (Hoàng Sa, Trường Sa) +// into their parent province geometries (Da Nang, Khanh Hoa). This fixes an upstream +// GIS data defect where the province-level API response excludes island territories. +func PatchIslandProvincesGeometry() { + postgresDB := db.GetPostgresDBConnection() + sapNhapGeoJSONObjectRepository := sapNhapR.NewSapNhapGeoJSONObjectRepository(postgresDB) + vnRepo := vnRepo.NewVnProvincesTmpRepository(postgresDB) + sapNhapService := sapNhapService.NewSapNhapService(vnRepo, sapNhapGeoJSONObjectRepository, postgresDB) + + log.Println("ℹ️ Patching island province geometries (Hoàng Sa → Da Nang, Trường Sa → Khanh Hoa)...") + if err := sapNhapService.PatchIslandProvincesGeometry(context.Background()); err != nil { + log.Fatalf("Failed to patch island province geometries: %v", err) + panic(err) + } + log.Println("✅ Island province geometry patching completed successfully") +} +``` + +### 3. Integration into main.go + +**File:** `dataset-generation-scripts/main.go` + +Add the patch step after `FetchGISDataFromSapNhapBando()` and before `GenerateGISSQLDatasets()`: + +```go +if (INCLUDE_GIS) { + db.BootstrapGISDataStructure() + sapnhap.BackfillProvinceAndWardCodesInSapNhapGeojsonObjects() + sapnhap.FetchGISDataFromSapNhapBando() + sapnhap.PatchIslandProvincesGeometry() // ← NEW: merge islands into provinces + dataset_writer.GenerateGISSQLDatasets() +} +``` + +## Expected Result + +After the patch: + +| Province | Before bbox | After bbox (expected) | `ST_Contains(island)` | +|----------|------------|----------------------|----------------------| +| Da Nang (48) | `[107.21, 14.95, 108.74, 16.23]` | `[107.21, 14.95, 112.74, 17.12]` | `true` | +| Khanh Hoa (56) | `[108.55, 11.31, 109.46, 12.87]` | `[108.55, 7.18, 117.83, 12.87]` | `true` | + +The province geometries will spatially contain all their administrative subdivisions, including the Paracel and Spratly Islands. + +## Testing + +### Manual verification (SQL queries) + +After running the generation script, verify with: + +```sql +-- Verify containment after patch +SELECT + 'Hoang Sa within Da Nang' as check_name, + ST_Contains( + (SELECT geom FROM sapnhap_geojson_objects WHERE ma = '48'), + (SELECT geom FROM sapnhap_geojson_objects WHERE ma = '20333') + ) as is_contained +UNION ALL +SELECT + 'Truong Sa within Khanh Hoa', + ST_Contains( + (SELECT geom FROM sapnhap_geojson_objects WHERE ma = '56'), + (SELECT geom FROM sapnhap_geojson_objects WHERE ma = '22736') + ); +-- Expected: both true + +-- Verify bbox expansion +SELECT ma, ten, + ST_XMin(bbox) as xmin, ST_YMin(bbox) as ymin, + ST_XMax(bbox) as xmax, ST_YMax(bbox) as ymax +FROM sapnhap_geojson_objects +WHERE ma IN ('48', '56'); +-- Expected: Da Nang xmax ~112.74, Khanh Hoa xmin ~109.47, ymin ~7.18, xmax ~117.83 +``` + +### Unit test + +Add a test in `dataset-generation-scripts/internal/sapnhap_bando/service/sapnhap_test.go` that verifies: +1. After `PatchIslandProvincesGeometry()`, `ST_Contains` returns `true` for both island-province pairs +2. The province bbox has expanded to include the island territories + +## Scope + +- **In scope:** Merge Hoàng Sa geometry into Da Nang, merge Trường Sa geometry into Khanh Hoa +- **Out of scope:** Fixing boundary precision issues for other wards (those are `ST_Contains` false but `ST_Intersects` true — a different, minor issue not addressed by this patch) \ No newline at end of file diff --git a/docs/gis/gis_example_query.md b/docs/gis/gis_example_query.md index a19b5814..f4c93646 100644 --- a/docs/gis/gis_example_query.md +++ b/docs/gis/gis_example_query.md @@ -140,3 +140,61 @@ WHERE ST_Intersects( )::geometry ); ``` + +### Export Provinces/Wards as GeoJSON + +```sql +SELECT CONCAT(p.code, '_', p.code_name), p.name, +json_build_object( + 'type', 'FeatureCollection', + 'features', json_build_array( + json_build_object( + 'type', 'Feature', + 'bbox', ARRAY[ + ST_XMin(gp.bbox), + ST_YMin(gp.bbox), + ST_XMax(gp.bbox), + ST_YMax(gp.bbox) + ], + 'geometry', ST_AsGeoJSON(gp.geom)::json, + 'properties', json_build_object( + 'unit_name', p.name, + 'unit_code', gp.province_code, + 'unit_code_name', p.code_name, + 'gis_server_id', gp.gis_server_id, + 'area_km2', gp.area_km2 + ) + ) + ) + ) AS geojson +FROM provinces p +INNER JOIN gis_provinces gp +ON p.code = gp.province_code + +SELECT CONCAT(w.code, '_', w.code_name), w.name, +json_build_object( + 'type', 'FeatureCollection', + 'features', json_build_array( + json_build_object( + 'type', 'Feature', + 'bbox', ARRAY[ + ST_XMin(gw.bbox), + ST_YMin(gw.bbox), + ST_XMax(gw.bbox), + ST_YMax(gw.bbox) + ], + 'geometry', ST_AsGeoJSON(gw.geom)::json, + 'properties', json_build_object( + 'unit_name', w.name, + 'unit_code', gw.ward_code, + 'unit_code_name', w.code_name, + 'gis_server_id', gw.gis_server_id, + 'area_km2', gw.area_km2 + ) + ) + ) + ) AS geojson +FROM wards w +INNER JOIN gis_wards gw +ON w.code = gw.ward_code; +``` \ No newline at end of file