CSV to GeoJSON Converter

A CSV to GeoJSON converter turns a spreadsheet of coordinates into a FeatureCollection that Leaflet, Mapbox, QGIS and geojson.io can draw. This one finds your latitude and longitude columns by name, writes each position longitude first as RFC 7946 requires, gives every other column its real type, and counts and names any row it could not plot. The file never leaves your machine.

Coordinates in one column, or a projected grid to convert first? Open the app

The axis order that ruins an afternoon

Here is the bug. You export a list of stores from a database. The columns are latitude and longitude, in that order, because that is how everyone says it and how every address lookup returns it. You write ten lines of Python to wrap each row in a Feature, you paste the result into geojson.io, and every one of your London stores is sitting in the Gulf of Guinea, a few hundred kilometres off the coast of Ghana.

That spot is zero degrees north, zero degrees east. It is where a coordinate lands when the two numbers are small and swapped. GeoJSON positions are [longitude, latitude]. RFC 7946 is explicit about it, and it is the reverse of the order a person uses out loud, the order a spreadsheet column is usually in, and the order Google Maps shows in its own URL bar. Nothing errors. The file is structurally valid. It is just wrong, and it stays wrong until somebody looks at a map.

This converter reads your headings and applies the order for you. A column called latitude, lat or y is the latitude; a column called longitude, long, lon, lng or x is the longitude. Whichever way round they sit in your file, the output puts longitude first. When the detection cannot find them, or finds the wrong ones, the two dropdowns list the columns of the file in front of you and you pick in a second.

A worked example

Say you have this, four columns and a padded code, which is the shape a store or sensor list usually arrives in:

name,latitude,longitude,population,code
London,51.5072,-0.1276,9648000,0047
Sydney,-33.8688,151.2093,5185000,0250
Cairo,30.0444,31.2357,21750000,0969

Drop it in and you get this back. Look at three things in it:

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "name": "London",
        "population": 9648000,
        "code": "0047"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [-0.1276, 51.5072]
      }
    },
    ...
  ]
}

First, the coordinates are [-0.1276, 51.5072]. Longitude, then latitude, the reverse of the column order in the source. Second, population is the number 9648000, not the string "9648000", so a Mapbox expression can size a circle by it and a QGIS filter can compare it. Third, code is still the string "0047". A leading zero means the value is an identifier rather than a quantity, and turning it into 47 would break the join back to whatever system issued it.

The latitude and longitude columns do not appear in properties. They are already the geometry, and repeating them is noise in every popup that renders the whole property bag. There is a switch to keep them if a downstream tool expects them.

Rows that cannot be plotted are named, not dropped

Real coordinate files are dirty in four specific ways, and all four produce a row that cannot become a Feature. A blank cell where a geocoder gave up. A value like N/A or north that is text rather than a number. A latitude of 910, which is a data-entry slip. And a whole file of numbers in the hundreds of thousands, which is a projected coordinate system that somebody labelled x and y.

Each of those rows is left out, because there is nothing honest to put in its place. What matters is what you are told. The warning gives the count against the total, then lists the first ten offenders by their line number in the original file along with the value that failed:

3 of 400 rows had no usable coordinate and were left out:
row 14: coordinate cell is blank;
row 88: "north", "-0.12" is not a number;
row 251: latitude 910 is outside -90 to 90.

A line number and the offending value is enough to open the CSV and fix it. A converter that quietly hands back 397 features from a 400-row file, with no note anywhere, is the reason people stop trusting these tools.

The out-of-range check is the one that saves the most time. If every row is reported as out of range, your file is almost certainly in a national grid, in metres, and it needs reprojecting before GeoJSON can hold it. Saying that immediately beats producing an empty map and letting you hunt for the reason.

Lines and polygons from grouped rows

A table of coordinates is not always a table of points. A GPS trace is thousands of rows that together are one line. A delivery run is a few dozen rows per driver, each driver being a separate route. A set of zone boundaries is a ring of vertices per zone. Emitting a Point per row for any of those gives you a cloud of dots and loses the thing you cared about.

Choose LineString or Polygon and pick a grouping column. Every distinct value in that column becomes one feature, built from its rows in the order they appear in the file. A trip_id column gives one LineString per trip; a zone column gives one Polygon per zone. The group's first row supplies the feature's properties, and the group value becomes its name.

Polygons get one thing done for you that catches most people out: the ring is closed. A GeoJSON LinearRing has to end where it started, with the first position repeated as the last. Leaving that out is the most common reason a polygon is rejected by a validator or drawn as an open shape, and asking a person to duplicate the first row of every zone by hand is not a conversion. If the last vertex already matches the first, nothing is added.

A group with too few points for the geometry you asked for is reported rather than emitted as something malformed. Two points cannot be a polygon, one point cannot be a line, and the warning names the groups that fell short with their point counts.

Coordinate systems, briefly

GeoJSON has exactly one coordinate reference system and no way to declare another. RFC 7946 fixes it at WGS 84 decimal degrees, the system usually written as EPSG:4326, and it removed the crs member that the older 2008 specification allowed. If you find a file with a crs block in it, that file predates the RFC and most current readers ignore the block entirely.

Nothing here reprojects. A converter that guessed at your source projection and silently transformed your coordinates would be far more dangerous than one that refuses, because the output would look plausible and be wrong by a few hundred metres. If your data is in British National Grid, State Plane, UTM or a local municipal grid, run it through a reprojection first. QGIS, GDAL's ogr2ogr and the pyproj library all do this properly, and any of them will hand you back degrees you can bring straight here.

Coordinates are written to seven decimal places, which is roughly a centimetre. That is finer than any consumer GPS and much finer than anything that arrives in a spreadsheet. Beyond seven you are only writing floating point noise into the file, and 12.340000000000001 is not more precise than 12.34, only longer.

Questions

Why does my GeoJSON list longitude before latitude?

Because RFC 7946 says so. A GeoJSON position is [longitude, latitude], which is the opposite of the order almost every spreadsheet, GPS reading and street address uses. This is the single most common reason a converted file opens without an error and plots Paris in the Indian Ocean. The order is applied for you here, so a file whose columns are named latitude and longitude comes out correct without you thinking about it.

How does it know which column holds the latitude?

By name, case-insensitively. Latitude, lat, y and a few variants are checked for the latitude, and longitude, long, lon, lng and x for the longitude. If your headings are something else, the two dropdowns list the columns of the file you just loaded and you pick. The dropdowns also show what the detection chose, so the setting on screen always agrees with the file you are about to download.

What happens to a row with a broken coordinate?

It is left out of the FeatureCollection, counted, and named. The warning tells you how many of how many rows were dropped and lists the first ten by their line number in the original file, with the offending value: row 14 says north rather than a number, row 22 has a latitude of 910. A converter that silently produces 380 features from 400 rows is worse than one that fails.

Can it make lines and polygons, or only points?

All three. Point per row is the default. Choose LineString and pick a grouping column and every distinct value in that column becomes one line, with its rows joined in file order, which is how a GPS trace with a trip identifier becomes a set of routes. Polygon works the same way and closes each ring for you, since an unclosed ring is the most common reason a polygon is rejected.

Are the properties typed, or is everything a string?

Typed, and decided once per column rather than cell by cell. A population column becomes JSON numbers, a flag column becomes true and false, and a postcode column holding 01730 stays the string 01730 because a leading zero means it is a code and not a quantity. A column that mixes 9.99 with 12.50 stays text, because 12.50 written as a number is 12.5 and the cent is gone.

What coordinate system does the output use?

WGS 84 decimal degrees, EPSG:4326, which is the only system RFC 7946 allows. Nothing is reprojected. If your file holds easting and northing in metres from a national grid, those values are outside the legal degree ranges and every row will be reported as skipped, which is the tool telling you to reproject first rather than writing a file that plots nothing.

Is my file uploaded anywhere?

No. The conversion runs in your browser tab. The file is never sent to a server, nothing is stored between visits, and there is no row cap or daily quota. You can check this yourself: load the page, turn off your network, and convert a file.

Convert your CSV to GeoJSON

No sign-up, no upload, no row cap. Longitude first, properties typed, every skipped row accounted for.

Back to the converter