Here is an internal function that I am about to add to Bioconductor's CAGEr package, to solve the same problem.
#' ranges2genes
#'
#' Assign gene symbol(s) to Genomic Ranges.
#'
#' @param ranges Genomics Ranges object, for example extracted from a
#' RangedSummarizedExperiment object with the \code{rowRanges}
#' command.
#'
#' @param genes A \code{\link{GRanges}} object containing \code{gene_name} metadata.
#'
#' @return A character vector of same length as the GRanges object, indicating
#' one gene symbol or a comma-separated list of gene symbols for each
#' range.
#'
#' @importFrom GenomicRanges findOverlaps
#' @importFrom S4Vectors List Rle unstrsplit
#' @importFrom IRanges extractList
#'
#' @examples
#' # Example for Biostars
#' # Imagine that nucleotides are represented by a GRanges object called "gr"
#' # Download GENCODE from Bioc's annotation hub:
#' ah <- AnnotationHub::AnnotationHub()
#' gff <- ah[["AH49556"]]
#' Instead one can also use rtracklayer::import.gff, etc...
#'
#' Annotate the GRanges:
#' gr$geneSymbols <- ranges2genes(gr, gff)
ranges2genes <- function(ranges, genes) {
if (is.null(genes$gene_name))
stop("Annotation must contain ", dQuote("gene_name"), " metdata.")
gnames <- findOverlaps(ranges, genes)
gnames <- as(gnames, "List")
gnames <- extractList(genes$gene_name, gnames)
gnames <- unique(gnames)
gnames <- unstrsplit(gnames, ";")
Rle(gnames)
}
use bedtools to intersect coordinates with gene coordinates