---
title: "Handling City Filter Uploads · RUAL Documentation"
description: "Parse uploaded CSV or XLSX locations and keep only the rows inside your city or radius."
canonical: https://docs.rual.nl/tutorials/city-filter-uploads
language: en
---

# Handling City Filter Uploads

Accept a CSV or XLSX of locations over an API, parse it into rows, keep only what falls inside the target city or radius, and store or reply with the result.

This tutorial builds an upload endpoint with a geo gate: a partner posts a file of locations, and only the rows relevant to your city ever reach storage. The same shape covers delivery zones, service areas and region-scoped imports. It assumes a registered endpoint; see [Build Your First API](https://docs.rual.nl/cluster/api-quickstart). The geo side of this page runs offline against datasets on the node; background in [Places and Geo Search](https://docs.rual.nl/blueprints/places).

### 1. Upload and Parse

[`function_get_file_from_form`](https://docs.rual.nl/block-types/files/function_get_file_from_form) (get) takes the uploaded file off the multipart form by field name. Parse it into an array of row objects:

- [`file_parse_csv`](https://docs.rual.nl/block-types/files/file_parse_csv) (parse CSV) detects the separator, reads headers, and outputs one object per row. Its `error` pin catches files that are not CSV at all.

- [`file_parse_xlsx`](https://docs.rual.nl/block-types/files/file_parse_xlsx) (parse XLSX) and [`file_parse_xlsx_to_array`](https://docs.rual.nl/block-types/files/file_parse_xlsx_to_array) do the same for Excel uploads.

Only accepting the file for later? [`function_save_file_from_form`](https://docs.rual.nl/block-types/files/function_save_file_from_form) (save) stores it as an asset instead of parsing it in the request.

### 2. The City Filter

Two blocks answer "is this row inside my area", depending on whether the area is a city or a drawn radius:

An uploaded file becomes rows: [get](https://docs.rual.nl/block-types/files/function_get_file_from_form) takes the file from the form, [parse CSV](https://docs.rual.nl/block-types/files/file_parse_csv) turns it into an array of objects, and [filter by geo distance](https://docs.rual.nl/block-types/array/array_filter_geo_distance) keeps only the rows whose location lies within their own radius of [from city](https://docs.rual.nl/block-types/geopoint/geo_point_from_city) (here Breda). [array length](https://docs.rual.nl/block-types/array/array_length) counts what survived.

![Studio canvas example for the parse CSV block: upload, parse, filter by city.](https://docs.rual.nl/canvas-examples/file_parse_csv.png)

On the canvas: [`geo_point_from_city`](https://docs.rual.nl/block-types/geopoint/geo_point_from_city) (from city) resolves the target city to its centre point, and [`array_filter_geo_distance`](https://docs.rual.nl/block-types/array/array_filter_geo_distance) (filter by geo distance) keeps the rows whose own location field lies within reach. The block reads the location and the allowed distance from each row itself: `location_key` names the field holding the row's geo point, `distance_key` the field holding its radius. Rows carry their own reach, so a single pass handles "within 5 km" for one row and "within 500 m" for the next. What does not match comes out on `out_of_range`, so you can report or log the rejects instead of dropping them silently.

Rows that store plain lat/lng numbers instead of a geo point need one conversion first: [`geo_point_lat_lng_to_point`](https://docs.rual.nl/block-types/geopoint/geo_point_lat_lng_to_point) (create). For a fixed radius applied to every row, test each row's point with [`condition_geo_is_point_in_circle`](https://docs.rual.nl/block-types/condition/condition_geo_is_point_in_circle) (point in radius) inside an iterator, or filter at the storage level with [`query_bool_geo_distance`](https://docs.rual.nl/block-types/query/query_bool_geo_distance) so documents outside the area are never read at all.

### 3. Validate, Enrich, Store

Before the surviving rows reach storage:

- Check each row's point with [`geo_point_validate`](https://docs.rual.nl/block-types/geopoint/geo_point_validate) (is valid): uploads regularly contain empty or swapped coordinates, and one bad row should not poison the batch.

- Enrich what you keep with [`geo_point_reverse_geocode`](https://docs.rual.nl/block-types/geopoint/geo_point_reverse_geocode) (reverse geocode), which resolves the point to street, city, province and country. Writing those onto the document with [`mutations_set_bp_field_multiple`](https://docs.rual.nl/block-types/mutations/mutations_set_bp_field_multiple) (set fields) makes the rows searchable by city name later, a combination production blueprints use directly on the upload path.

- Write the batch with [`function_upsert_document_from_mutations`](https://docs.rual.nl/block-types/storage/function_upsert_document_from_mutations) (upsert document) inside a [`function_foreach`](https://docs.rual.nl/block-types/foreach/function_foreach) (foreach) loop: re-uploading the same file then updates rows instead of duplicating them.

Reply with a small summary object: rows received, kept, rejected. Build it with [`object_new_fields`](https://docs.rual.nl/block-types/object/object_new_fields) (new object) fed by [`array_length`](https://docs.rual.nl/block-types/array/array_length) (array length) on both output arrays, and send it with [`httpconnection_set_json`](https://docs.rual.nl/block-types/json/httpconnection_set_json) (reply in JSON), the response backbone from [Common Blueprint Patterns](https://docs.rual.nl/blueprints/common-patterns#api-response).

### Notes From Practice

- Large files do not belong in the request: parse, reply 202, and filter and store through [`function_custom_execute_from_queue`](https://docs.rual.nl/block-types/function%20execution/function_custom_execute_from_queue) (execute in queue). See [Queue](https://docs.rual.nl/blueprints/queue).

- The geo blocks are offline lookups, so filtering ten thousand rows costs no external calls and no meaningful time.

- Scope the endpoint and rate limit it; an open upload endpoint is an invitation. See [Remote Access Control](https://docs.rual.nl/blueprints/remote-access-control).

- Branch on the parse block's `error` output before touching the array: replying 400 with "not a CSV" beats storing nothing and replying 200.

### Frequently asked

**How do I accept a CSV upload through a RUAL API?**

Take the file off the multipart form with function_get_file_from_form, then parse it with file_parse_csv into an array of row objects. The parse block detects the separator and reports files that are not CSV on its error pin.

**How do I keep only uploaded rows inside a city in RUAL?**

Resolve the city with geo_point_from_city and filter the parsed rows with array_filter_geo_distance, which reads each row's location and allowed radius from fields you name. Rows that fall outside come out on the out_of_range pin, and all of it runs offline against datasets on the node.
